Merge branch '210_twitter_api_uploads_alt_text' into 'develop'
[akkoma] / lib / pleroma / web / twitter_api / twitter_api_controller.ex
index 244d31dda75ed88d70a68ef83a98ed691a798339..0ccf937b0f003b677797bc83fcdba8019e070c4c 100644 (file)
@@ -4,13 +4,14 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
   alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
-  alias Pleroma.{Repo, Activity, User, Notification}
+  alias Pleroma.{Repo, Activity, Object, User, Notification}
   alias Pleroma.Web.ActivityPub.ActivityPub
   alias Pleroma.Web.ActivityPub.Utils
   alias Ecto.Changeset
 
   require Logger
 
+  plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
   action_fallback(:errors)
 
   def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
@@ -79,7 +80,9 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
       |> Map.put("blocking_user", user)
       |> Map.put("user", user)
 
-    activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
+    activities =
+      ActivityPub.fetch_activities([user.ap_id | user.following], params)
+      |> ActivityPub.contain_timeline(user)
 
     conn
     |> render(ActivityView, "index.json", %{activities: activities, for: user})
@@ -123,6 +126,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     |> render(ActivityView, "index.json", %{activities: activities, for: user})
   end
 
+  def dm_timeline(%{assigns: %{user: user}} = conn, params) do
+    query =
+      ActivityPub.fetch_activities_query(
+        [user.ap_id],
+        Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"})
+      )
+
+    activities = Repo.all(query)
+
+    conn
+    |> render(ActivityView, "index.json", %{activities: activities, for: user})
+  end
+
   def notifications(%{assigns: %{user: user}} = conn, params) do
     notifications = Notification.for_user(user, params)
 
@@ -130,6 +146,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     |> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
   end
 
+  def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
+    Notification.set_read_up_to(user, latest_id)
+
+    notifications = Notification.for_user(user, params)
+
+    conn
+    |> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
+  end
+
+  def notifications_read(%{assigns: %{user: user}} = conn, _) do
+    bad_request_reply(conn, "You need to specify latest_id")
+  end
+
   def follow(%{assigns: %{user: user}} = conn, params) do
     case TwitterAPI.follow(user, params) do
       {:ok, user, followed, _activity} ->
@@ -197,16 +226,51 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     end
   end
 
-  def upload(conn, %{"media" => media}) do
-    response = TwitterAPI.upload(media)
+  @doc """
+  Updates metadata of uploaded media object.
+  Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
+  """
+  def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
+    object = Repo.get(Object, id)
+    description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
+
+    {conn, status, response_body} =
+      cond do
+        !object ->
+          {halt(conn), :not_found, ""}
+
+        !Object.authorize_mutation(object, user) ->
+          {halt(conn), :forbidden, "You can only update your own uploads."}
+
+        !is_binary(description) ->
+          {conn, :not_modified, ""}
+
+        true ->
+          new_data = Map.put(object.data, "name", description)
+
+          {:ok, _} =
+            object
+            |> Object.change(%{data: new_data})
+            |> Repo.update()
+
+          {conn, :no_content, ""}
+      end
+
+    conn
+    |> put_status(status)
+    |> json(response_body)
+  end
+
+  def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
+    response = TwitterAPI.upload(media, user)
 
     conn
     |> put_resp_content_type("application/atom+xml")
     |> send_resp(200, response)
   end
 
-  def upload_json(conn, %{"media" => media}) do
-    response = TwitterAPI.upload(media, "json")
+  def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
+    response = TwitterAPI.upload(media, user, "json")
 
     conn
     |> json_reply(200, response)
@@ -261,7 +325,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
   end
 
   def update_avatar(%{assigns: %{user: user}} = conn, params) do
-    {:ok, object} = ActivityPub.upload(params)
+    {: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)
@@ -270,10 +334,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
   end
 
   def update_banner(%{assigns: %{user: user}} = conn, params) do
-    with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}),
-         new_info <- Map.put(user.info, "banner", object.data),
-         change <- User.info_changeset(user, %{info: new_info}),
-         {:ok, user} <- User.update_and_set_cache(change) 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
       response = %{url: href} |> Jason.encode!()
@@ -284,10 +349,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
   end
 
   def update_background(%{assigns: %{user: user}} = conn, params) do
-    with {:ok, object} <- ActivityPub.upload(params),
-         new_info <- Map.put(user.info, "background", object.data),
-         change <- User.info_changeset(user, %{info: new_info}),
-         {:ok, _user} <- User.update_and_set_cache(change) 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
       response = %{url: href} |> Jason.encode!()
 
@@ -309,32 +375,32 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     end
   end
 
-  def update_most_recent_notification(%{assigns: %{user: user}} = conn, %{"id" => id}) do
-    with id when is_number(id) <- String.to_integer(id),
-         info <- user.info,
-         mrn <- max(id, user.info["most_recent_notification"] || 0),
-         updated_info <- Map.put(info, "most_recent_notification", mrn),
-         changeset <- User.info_changeset(user, %{info: updated_info}),
-         {:ok, _user} <- User.update_and_set_cache(changeset) do
-      conn
-      |> json_reply(200, Jason.encode!(mrn))
-    else
-      _e -> bad_request_reply(conn, "Can't update.")
-    end
-  end
-
-  def followers(conn, params) do
-    with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
+  def followers(%{assigns: %{user: for_user}} = conn, params) do
+    with {:ok, user} <- TwitterAPI.get_user(for_user, params),
          {:ok, followers} <- User.get_followers(user) do
+      followers =
+        cond do
+          for_user && user.id == for_user.id -> followers
+          user.info.hide_network -> []
+          true -> followers
+        end
+
       render(conn, UserView, "index.json", %{users: followers, for: conn.assigns[:user]})
     else
       _e -> bad_request_reply(conn, "Can't get followers")
     end
   end
 
-  def friends(conn, params) do
+  def friends(%{assigns: %{user: for_user}} = conn, params) do
     with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
          {:ok, friends} <- User.get_friends(user) do
+      friends =
+        cond do
+          for_user && user.id == for_user.id -> friends
+          user.info.hide_network -> []
+          true -> friends
+        end
+
       render(conn, UserView, "index.json", %{users: friends, for: conn.assigns[:user]})
     else
       _e -> bad_request_reply(conn, "Can't get friends")
@@ -410,53 +476,41 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     json(conn, [])
   end
 
-  def update_profile(%{assigns: %{user: user}} = conn, params) do
-    params =
-      if bio = params["description"] do
-        mentions = Formatter.parse_mentions(bio)
-        tags = Formatter.parse_tags(bio)
-
-        emoji =
-          (user.info["source_data"]["tag"] || [])
-          |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
-          |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
-            {String.trim(name, ":"), url}
-          end)
-
-        bio_html = CommonUtils.format_input(bio, mentions, tags, "text/plain")
-        Map.put(params, "bio", bio_html |> Formatter.emojify(emoji))
-      else
-        params
-      end
-
-    user =
-      if locked = params["locked"] do
-        with locked <- locked == "true",
-             new_info <- Map.put(user.info, "locked", locked),
-             change <- User.info_changeset(user, %{info: new_info}),
-             {:ok, user} <- User.update_and_set_cache(change) do
-          user
+  defp build_info_cng(user, params) do
+    info_params =
+      ["no_rich_text", "locked", "hide_network"]
+      |> Enum.reduce(%{}, fn key, res ->
+        if value = params[key] do
+          Map.put(res, key, value == "true")
         else
-          _e -> user
+          res
         end
-      else
-        user
-      end
+      end)
 
-    user =
-      if default_scope = params["default_scope"] do
-        with new_info <- Map.put(user.info, "default_scope", default_scope),
-             change <- User.info_changeset(user, %{info: new_info}),
-             {:ok, user} <- User.update_and_set_cache(change) do
-          user
-        else
-          _e -> user
-        end
+    info_params =
+      if value = params["default_scope"] do
+        Map.put(info_params, "default_scope", value)
       else
-        user
+        info_params
       end
 
+    User.Info.profile_update(user.info, info_params)
+  end
+
+  defp parse_profile_bio(user, params) do
+    if bio = params["description"] do
+      Map.put(params, "bio", User.parse_bio(bio, user))
+    else
+      params
+    end
+  end
+
+  def update_profile(%{assigns: %{user: user}} = conn, params) do
+    params = parse_profile_bio(user, params)
+    info_cng = build_info_cng(user, params)
+
     with changeset <- User.update_changeset(user, params),
+         changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
          {:ok, user} <- User.update_and_set_cache(changeset) do
       CommonAPI.update(user)
       render(conn, UserView, "user.json", %{user: user, for: user})
@@ -474,6 +528,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     |> render(ActivityView, "index.json", %{activities: activities, for: user})
   end
 
+  def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
+    users = User.search(query, true)
+
+    conn
+    |> render(UserView, "index.json", %{users: users, for: user})
+  end
+
   defp bad_request_reply(conn, error_message) do
     json = error_json(conn, error_message)
     json_reply(conn, 400, json)
@@ -490,6 +551,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
     json_reply(conn, 403, json)
   end
 
+  def only_if_public_instance(conn = %{conn: %{assigns: %{user: _user}}}, _), do: conn
+
+  def only_if_public_instance(conn, _) do
+    if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
+      conn
+    else
+      conn
+      |> forbidden_json_reply("Invalid credentials.")
+      |> halt()
+    end
+  end
+
   defp error_json(conn, error_message) do
     %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
   end