do nothing if configuration is skipped
[akkoma] / lib / pleroma / web / mastodon_api / mastodon_api_controller.ex
index d506c4a41e08318446ebbed191390aedbdb3f34e..ac8f794e9275c0d6e15874cf78ff1d68f79063b0 100644 (file)
@@ -2,14 +2,19 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   use Pleroma.Web, :controller
   alias Pleroma.{Repo, Activity, User, Notification, Stats}
   alias Pleroma.Web
-  alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView}
+  alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
   alias Pleroma.Web.ActivityPub.ActivityPub
+  alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.{CommonAPI, OStatus}
   alias Pleroma.Web.OAuth.{Authorization, Token, App}
   alias Comeonin.Pbkdf2
   import Ecto.Query
   require Logger
 
+  @httpoison Application.get_env(:pleroma, :httpoison)
+
+  action_fallback(:errors)
+
   def create_app(conn, params) do
     with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
          {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
@@ -69,6 +74,20 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         user
       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
+        else
+          _e -> user
+        end
+      else
+        user
+      end
+
     with changeset <- User.update_changeset(user, params),
          {:ok, user} <- User.update_and_set_cache(changeset) do
       if original_user != user do
@@ -112,7 +131,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
       email: Keyword.get(@instance, :email),
       urls: %{
-        streaming_api: String.replace(Web.base_url(), ["http", "https"], "wss")
+        streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
       },
       stats: Stats.get_stats(),
       thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
@@ -134,6 +153,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       %{
         "shortcode" => shortcode,
         "static_url" => url,
+        "visible_in_picker" => true,
         "url" => url
       }
     end)
@@ -144,7 +164,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     json(conn, mastodon_emoji)
   end
 
-  defp add_link_headers(conn, method, activities, param \\ false) do
+  defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
     last = List.last(activities)
     first = List.first(activities)
 
@@ -155,13 +175,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       {next_url, prev_url} =
         if param do
           {
-            mastodon_api_url(Pleroma.Web.Endpoint, method, param, max_id: min),
-            mastodon_api_url(Pleroma.Web.Endpoint, method, param, since_id: max)
+            mastodon_api_url(
+              Pleroma.Web.Endpoint,
+              method,
+              param,
+              Map.merge(params, %{max_id: min})
+            ),
+            mastodon_api_url(
+              Pleroma.Web.Endpoint,
+              method,
+              param,
+              Map.merge(params, %{since_id: max})
+            )
           }
         else
           {
-            mastodon_api_url(Pleroma.Web.Endpoint, method, max_id: min),
-            mastodon_api_url(Pleroma.Web.Endpoint, method, since_id: max)
+            mastodon_api_url(
+              Pleroma.Web.Endpoint,
+              method,
+              Map.merge(params, %{max_id: min})
+            ),
+            mastodon_api_url(
+              Pleroma.Web.Endpoint,
+              method,
+              Map.merge(params, %{since_id: max})
+            )
           }
         end
 
@@ -189,10 +227,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def public_timeline(%{assigns: %{user: user}} = conn, params) do
+    local_only = params["local"] in [true, "True", "true", "1"]
+
     params =
       params
       |> Map.put("type", ["Create", "Announce"])
-      |> Map.put("local_only", params["local"] in [true, "True", "true", "1"])
+      |> Map.put("local_only", local_only)
       |> Map.put("blocking_user", user)
 
     activities =
@@ -200,33 +240,41 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.reverse()
 
     conn
-    |> add_link_headers(:public_timeline, activities)
+    |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
     |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
   end
 
-  def user_statuses(%{assigns: %{user: user}} = conn, params) do
-    with %User{ap_id: ap_id} <- Repo.get(User, params["id"]) do
-      params =
-        params
-        |> Map.put("type", ["Create", "Announce"])
-        |> Map.put("actor_id", ap_id)
-        |> Map.put("whole_db", true)
-
-      if params["pinned"] == "true" do
-        # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
-        activities = []
-      else
-        activities =
-          ActivityPub.fetch_public_activities(params)
-          |> Enum.reverse()
-      end
+  def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
+    with %User{} = user <- Repo.get(User, params["id"]) do
+      # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
+      activities =
+        if params["pinned"] == "true" do
+          []
+        else
+          ActivityPub.fetch_user_activities(user, reading_user, params)
+        end
 
       conn
       |> add_link_headers(:user_statuses, activities, params["id"])
-      |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
+      |> render(StatusView, "index.json", %{
+        activities: activities,
+        for: reading_user,
+        as: :activity
+      })
     end
   end
 
+  def dm_timeline(%{assigns: %{user: user}} = conn, _params) do
+    query =
+      ActivityPub.fetch_activities_query([user.ap_id], %{"type" => "Create", visibility: "direct"})
+
+    activities = Repo.all(query)
+
+    conn
+    |> add_link_headers(:dm_timeline, activities)
+    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
+  end
+
   def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
     with %Activity{} = activity <- Repo.get(Activity, id),
          true <- ActivityPub.visible_for_user?(activity, user) do
@@ -269,13 +317,30 @@ 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"])
       |> Map.put("no_attachment_links", true)
 
-    {:ok, activity} = CommonAPI.post(user, params)
+    idempotency_key =
+      case get_req_header(conn, "idempotency-key") do
+        [key] -> key
+        _ -> Ecto.UUID.generate()
+      end
+
+    {:ok, activity} =
+      Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
+
     render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
   end
 
@@ -291,27 +356,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   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) do
+    with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
       render(conn, StatusView, "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, _, _, %{data: %{"id" => id}}} = CommonAPI.unrepeat(ap_id_or_id, user),
+    with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
          %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
       render(conn, StatusView, "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),
+    with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
          %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
       render(conn, StatusView, "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),
+    with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
          %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
       render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
     end
@@ -396,10 +461,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   end
 
   def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
+    local_only = params["local"] in [true, "True", "true", "1"]
+
     params =
       params
       |> Map.put("type", "Create")
-      |> Map.put("local_only", !!params["local"])
+      |> Map.put("local_only", local_only)
       |> Map.put("blocking_user", user)
 
     activities =
@@ -407,7 +474,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       |> Enum.reverse()
 
     conn
-    |> add_link_headers(:hashtag_timeline, activities, params["tag"])
+    |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
     |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
   end
 
@@ -426,9 +493,56 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
+  def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
+    with {:ok, follow_requests} <- User.get_follow_requests(followed) do
+      render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user})
+    end
+  end
+
+  def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
+    with %User{} = follower <- Repo.get(User, id),
+         {:ok, follower} <- User.maybe_follow(follower, followed),
+         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
+         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
+         {:ok, _activity} <-
+           ActivityPub.accept(%{
+             to: [follower.ap_id],
+             actor: followed.ap_id,
+             object: follow_activity.data["id"],
+             type: "Accept"
+           }) do
+      render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
+    else
+      {:error, message} ->
+        conn
+        |> put_resp_content_type("application/json")
+        |> send_resp(403, Jason.encode!(%{"error" => message}))
+    end
+  end
+
+  def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
+    with %User{} = follower <- Repo.get(User, id),
+         %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
+         {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
+         {:ok, _activity} <-
+           ActivityPub.reject(%{
+             to: [follower.ap_id],
+             actor: followed.ap_id,
+             object: follow_activity.data["id"],
+             type: "Reject"
+           }) do
+      render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
+    else
+      {:error, message} ->
+        conn
+        |> put_resp_content_type("application/json")
+        |> send_resp(403, Jason.encode!(%{"error" => message}))
+    end
+  end
+
   def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
     with %User{} = followed <- Repo.get(User, id),
-         {:ok, follower} <- User.follow(follower, followed),
+         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
          {:ok, _activity} <- ActivityPub.follow(follower, followed) do
       render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
     else
@@ -441,7 +555,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 
   def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
     with %User{} = followed <- Repo.get_by(User, nickname: uri),
-         {:ok, follower} <- User.follow(follower, followed),
+         {:ok, follower} <- User.maybe_direct_follow(follower, followed),
          {:ok, _activity} <- ActivityPub.follow(follower, followed) do
       render(conn, AccountView, "account.json", %{user: followed})
     else
@@ -452,24 +566,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     end
   end
 
-  # TODO: Clean up and unify
   def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
     with %User{} = followed <- Repo.get(User, id),
-         {:ok, follower, follow_activity} <- User.unfollow(follower, followed),
-         {:ok, _activity} <-
-           ActivityPub.insert(%{
-             "type" => "Undo",
-             "actor" => follower.ap_id,
-             # get latest Follow for these users
-             "object" => follow_activity.data["id"]
-           }) do
+         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
+         {:ok, follower, _} <- User.unfollow(follower, followed) do
       render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
     end
   end
 
   def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
     with %User{} = blocked <- Repo.get(User, id),
-         {:ok, blocker} <- User.block(blocker, blocked) do
+         {:ok, blocker} <- User.block(blocker, blocked),
+         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
       render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
     else
       {:error, message} ->
@@ -481,7 +589,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
 
   def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
     with %User{} = blocked <- Repo.get(User, id),
-         {:ok, blocker} <- User.unblock(blocker, blocked) do
+         {:ok, blocker} <- User.unblock(blocker, blocked),
+         {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
       render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
     else
       {:error, message} ->
@@ -500,6 +609,72 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     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 search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
+    accounts = User.search(query, params["resolve"] == "true")
+
+    fetched =
+      if Regex.match?(~r/https?:/, query) do
+        with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
+          activities
+          |> Enum.filter(fn
+            %{data: %{"type" => "Create"}} -> true
+            _ -> false
+          end)
+        else
+          _e -> []
+        end
+      end || []
+
+    q =
+      from(
+        a in Activity,
+        where: fragment("?->>'type' = 'Create'", a.data),
+        where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
+        where:
+          fragment(
+            "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
+            a.data,
+            ^query
+          ),
+        limit: 20,
+        order_by: [desc: :id]
+      )
+
+    statuses = Repo.all(q) ++ fetched
+
+    tags_path = Web.base_url() <> "/tag/"
+
+    tags =
+      String.split(query)
+      |> 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, params["resolve"] == "true")
 
@@ -572,6 +747,102 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
   end
 
+  def get_lists(%{assigns: %{user: user}} = conn, opts) do
+    lists = Pleroma.List.for_user(user, opts)
+    res = ListView.render("lists.json", lists: lists)
+    json(conn, res)
+  end
+
+  def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
+      res = ListView.render("list.json", list: list)
+      json(conn, res)
+    else
+      _e -> json(conn, "error")
+    end
+  end
+
+  def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
+         {:ok, _list} <- Pleroma.List.delete(list) do
+      json(conn, %{})
+    else
+      _e ->
+        json(conn, "error")
+    end
+  end
+
+  def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
+    with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
+      res = ListView.render("list.json", list: list)
+      json(conn, res)
+    end
+  end
+
+  def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
+    accounts
+    |> Enum.each(fn account_id ->
+      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
+           %User{} = followed <- Repo.get(User, account_id) do
+        Pleroma.List.follow(list, followed)
+      end
+    end)
+
+    json(conn, %{})
+  end
+
+  def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
+    accounts
+    |> Enum.each(fn account_id ->
+      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
+           %User{} = followed <- Repo.get(Pleroma.User, account_id) do
+        Pleroma.List.unfollow(list, followed)
+      end
+    end)
+
+    json(conn, %{})
+  end
+
+  def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
+    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
+         {:ok, users} = Pleroma.List.get_following(list) do
+      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
+    end
+  end
+
+  def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
+    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
+         {:ok, list} <- Pleroma.List.rename(list, title) do
+      res = ListView.render("list.json", list: list)
+      json(conn, res)
+    else
+      _e ->
+        json(conn, "error")
+    end
+  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)
+
+      # adding title is a hack to not make empty lists function like a public timeline
+      activities =
+        ActivityPub.fetch_activities([title | following], params)
+        |> Enum.reverse()
+
+      conn
+      |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
+    else
+      _e ->
+        conn
+        |> put_status(403)
+        |> json(%{error: "Error."})
+    end
+  end
+
   def index(%{assigns: %{user: user}} = conn, _params) do
     token =
       conn
@@ -595,11 +866,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
             boost_modal: false,
             delete_modal: true,
             auto_play_gif: false,
-            reduce_motion: false
+            display_sensitive_media: false,
+            reduce_motion: false,
+            max_toot_chars: Keyword.get(@instance, :limit)
+          },
+          rights: %{
+            delete_others_notice: !!user.info["is_moderator"]
           },
           compose: %{
             me: "#{user.id}",
-            default_privacy: "public",
+            default_privacy: user.info["default_scope"] || "public",
             default_sensitive: false
           },
           media_attachments: %{
@@ -790,4 +1066,43 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
         nil
     end
   end
+
+  def errors(conn, _) do
+    conn
+    |> put_status(500)
+    |> json("Something went wrong")
+  end
+
+  @suggestions Application.get_env(:pleroma, :suggestions)
+
+  def suggestions(%{assigns: %{user: user}} = conn, _) do
+    api = Keyword.get(@suggestions, :third_party_engine, false)
+
+    if api do
+      host =
+        Application.get_env(:pleroma, Pleroma.Web.Endpoint)
+        |> Keyword.get(:url)
+        |> Keyword.get(:host)
+
+      user = user.nickname
+      url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
+
+      with {:ok, %{status_code: 200, body: body}} <-
+             @httpoison.get(url, [], timeout: 300_000, recv_timeout: 300_000),
+           {:ok, data} <- Jason.decode(body) do
+        data2 =
+          Enum.slice(data, 0, 40)
+          |> Enum.map(fn x ->
+            Map.put(x, "id", User.get_or_fetch(x["acct"]).id)
+          end)
+
+        conn
+        |> json(data2)
+      else
+        e -> Logger.error("Could not decode user at fetch #{url}, #{inspect(e)}")
+      end
+    else
+      json(conn, [])
+    end
+  end
 end