Merge branch 'develop' into tests/mastodon_api_controller.ex
[akkoma] / lib / pleroma / web / mastodon_api / controllers / mastodon_api_controller.ex
index c5f2819764e1498f67224362bc3c46800f87e399..0c2b8dbb7b143e360c41ff72e19488381939a98b 100644 (file)
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   use Pleroma.Web, :controller
 
   import Pleroma.Web.ControllerHelper,
-    only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3]
+    only: [json_response: 3, add_link_headers: 2, add_link_headers: 3]
 
   alias Ecto.Changeset
   alias Pleroma.Activity
@@ -147,6 +147,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       [
         :no_rich_text,
         :locked,
+        :hide_followers_count,
+        :hide_follows_count,
         :hide_followers,
         :hide_follows,
         :hide_favorites,
@@ -290,7 +292,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
-    with %User{} = user <- get_user_by_nickname_or_id(for_user, nickname_or_id),
+    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)
@@ -365,7 +367,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.reverse()
 
     conn
-    |> add_link_headers(:home_timeline, activities)
+    |> add_link_headers(activities)
     |> put_view(StatusView)
     |> render("index.json", %{activities: activities, for: user, as: :activity})
   end
@@ -384,13 +386,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.reverse()
 
     conn
-    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
+    |> 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 <- get_user_by_nickname_or_id(reading_user, params["id"]) do
+    with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
       params =
         params
         |> Map.put("tag", params["tagged"])
@@ -398,7 +400,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       activities = ActivityPub.fetch_user_activities(user, reading_user, params)
 
       conn
-      |> add_link_headers(:user_statuses, activities, params["id"])
+      |> add_link_headers(activities)
       |> put_view(StatusView)
       |> render("index.json", %{
         activities: activities,
@@ -422,11 +424,25 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Pagination.fetch_paginated(params)
 
     conn
-    |> add_link_headers(:dm_timeline, activities)
+    |> 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
@@ -447,8 +463,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
          grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
       result = %{
         ancestors:
-          StatusView.render(
-            "index.json",
+          StatusView.render("index.json",
             for: user,
             activities: grouped_activities[true] || [],
             as: :activity
@@ -456,8 +471,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
           |> Enum.reverse(),
         # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
         descendants:
-          StatusView.render(
-            "index.json",
+          StatusView.render("index.json",
             for: user,
             activities: grouped_activities[false] || [],
             as: :activity
@@ -471,7 +485,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with %Object{} = object <- Object.get_by_id(id),
+    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
@@ -523,7 +537,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
     with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
       conn
-      |> add_link_headers(:scheduled_statuses, scheduled_activities)
+      |> add_link_headers(scheduled_activities)
       |> put_view(ScheduledActivityView)
       |> render("index.json", %{scheduled_activities: scheduled_activities})
     end
@@ -595,7 +609,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         {:ok, activity} ->
           conn
           |> put_view(StatusView)
-          |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+          |> try_render("status.json", %{
+            activity: activity,
+            for: user,
+            as: :activity,
+            with_direct_conversation_id: true
+          })
       end
     end
   end
@@ -706,7 +725,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     notifications = MastodonAPI.get_notifications(user, params)
 
     conn
-    |> add_link_headers(:notifications, notifications)
+    |> add_link_headers(notifications)
     |> put_view(NotificationView)
     |> render("index.json", %{notifications: notifications, for: user})
   end
@@ -746,9 +765,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   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)
+    targets = User.get_all_by_ids(List.wrap(id))
 
     conn
     |> put_view(AccountView)
@@ -758,19 +775,15 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   # 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"]),
+  def update_media(
+        %{assigns: %{user: user}} = conn,
+        %{"id" => id, "description" => description} = _
+      )
+      when is_binary(description) do
+    with %Object{} = object <- Repo.get(Object, 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)
+         {:ok, %Object{data: data}} <- Object.update_data(object, %{"name" => description}) do
+      attachment_data = Map.put(data, "id", object.id)
 
       conn
       |> put_view(StatusView)
@@ -778,6 +791,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
+  def update_media(_conn, _data), do: {:error, :bad_request}
+
   def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
     with {:ok, object} <-
            ActivityPub.upload(
@@ -796,38 +811,28 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   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
+         %{type: "image"} = rendered <-
+           StatusView.render("attachment.json", %{attachment: attachment_data}),
+         {:ok, _user} = User.update_mascot(user, rendered) do
+      json(conn, rendered)
+    else
+      %{type: _type} = _ ->
         render_error(conn, :unsupported_media_type, "mascots can only be images")
-      end
+
+      e ->
+        e
     end
   end
 
   def get_mascot(%{assigns: %{user: user}} = conn, _params) do
     mascot = User.get_mascot(user)
 
-    conn
-    |> json(mascot)
+    json(conn, 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)
 
@@ -839,12 +844,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> 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)
 
@@ -856,6 +863,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> put_view(AccountView)
       |> render("accounts.json", %{for: user, users: users, as: :user})
     else
+      {:visible, false} -> {:error, :not_found}
       _ -> json(conn, [])
     end
   end
@@ -894,7 +902,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.reverse()
 
     conn
-    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
+    |> add_link_headers(activities, %{"local" => local_only})
     |> put_view(StatusView)
     |> render("index.json", %{activities: activities, for: user, as: :activity})
   end
@@ -910,7 +918,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         end
 
       conn
-      |> add_link_headers(:followers, followers, user)
+      |> add_link_headers(followers)
       |> put_view(AccountView)
       |> render("accounts.json", %{for: for_user, users: followers, as: :user})
     end
@@ -927,7 +935,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         end
 
       conn
-      |> add_link_headers(:following, followers, user)
+      |> add_link_headers(followers)
       |> put_view(AccountView)
       |> render("accounts.json", %{for: for_user, users: followers, as: :user})
     end
@@ -1119,10 +1127,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> put_view(AccountView)
       |> render("relationship.json", %{user: user, target: subscription_target})
     else
-      {:error, message} ->
-        conn
-        |> put_status(:forbidden)
-        |> json(%{error: message})
+      nil -> {:error, :not_found}
+      e -> e
     end
   end
 
@@ -1133,10 +1139,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> put_view(AccountView)
       |> render("relationship.json", %{user: user, target: subscription_target})
     else
-      {:error, message} ->
-        conn
-        |> put_status(:forbidden)
-        |> json(%{error: message})
+      nil -> {:error, :not_found}
+      e -> e
     end
   end
 
@@ -1152,7 +1156,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.reverse()
 
     conn
-    |> add_link_headers(:favourites, activities)
+    |> add_link_headers(activities)
     |> put_view(StatusView)
     |> render("index.json", %{activities: activities, for: user, as: :activity})
   end
@@ -1179,7 +1183,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         |> Enum.reverse()
 
       conn
-      |> add_link_headers(:favourites, activities)
+      |> add_link_headers(activities)
       |> put_view(StatusView)
       |> render("index.json", %{activities: activities, for: for_user, as: :activity})
     else
@@ -1200,15 +1204,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
 
     conn
-    |> add_link_headers(:bookmarks, bookmarks)
+    |> add_link_headers(bookmarks)
     |> put_view(StatusView)
     |> 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)
+
+    conn
+    |> put_view(ListView)
+    |> render("index.json", %{lists: lists})
   end
 
   def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
@@ -1363,7 +1369,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   @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)
@@ -1375,9 +1381,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   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: ".",
@@ -1399,31 +1403,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     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
@@ -1432,26 +1417,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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, [])
   end
 
-  def empty_object(conn, _) do
-    Logger.debug("Unimplemented, returning an empty object")
-    json(conn, %{})
-  end
-
   def get_filters(%{assigns: %{user: user}} = conn, _) do
     filters = Filter.get_filters(user)
     res = FilterView.render("filters.json", filters: filters)
@@ -1570,7 +1542,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       json(conn, data)
     else
       _e ->
-        %{}
+        json(conn, %{})
     end
   end
 
@@ -1623,7 +1595,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def account_register(%{assigns: %{app: _app}} = conn, _params) do
+  def account_register(%{assigns: %{app: _app}} = conn, _) do
     render_error(conn, :bad_request, "Missing parameters")
   end
 
@@ -1640,7 +1612,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       end)
 
     conn
-    |> add_link_headers(:conversations, participations)
+    |> add_link_headers(participations)
     |> json(conversations)
   end
 
@@ -1682,40 +1654,19 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  def try_render(conn, target, params)
-      when is_binary(target) do
+  defp 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
+  defp 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
-
-  defp get_user_by_nickname_or_id(for_user, nickname_or_id) do
-    restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
-
-    opts =
-      cond do
-        restrict_to_local == :all ->
-          [restrict_remote_nicknames: true]
-
-        restrict_to_local == false ->
-          []
-
-        restrict_to_local == :unauthenticated and match?(%User{}, for_user) ->
-          []
-
-        true ->
-          [restrict_remote_nicknames: true]
-      end
-
-    User.get_cached_by_nickname_or_id(nickname_or_id, opts)
-  end
 end