Merge branch 'develop' into 'reactions'
authorlain <lain@soykaf.club>
Sun, 6 Oct 2019 08:11:47 +0000 (08:11 +0000)
committerlain <lain@soykaf.club>
Sun, 6 Oct 2019 08:11:47 +0000 (08:11 +0000)
# Conflicts:
#   CHANGELOG.md

28 files changed:
docs/API/differences_in_mastoapi_responses.md
lib/pleroma/conversation.ex
lib/pleroma/conversation/participation.ex
lib/pleroma/user.ex
lib/pleroma/user/info.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/mastodon_api/controllers/account_controller.ex
lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
lib/pleroma/web/mastodon_api/controllers/status_controller.ex
lib/pleroma/web/mastodon_api/views/account_view.ex
lib/pleroma/web/router.ex
priv/repo/migrations/20191005165212_add_unread_conversation_count_to_user_info.exs [new file with mode: 0644]
test/conversation/participation_test.exs
test/fixtures/bogus-mastodon-announce.json [new file with mode: 0644]
test/fixtures/mastodon-announce-private.json [new file with mode: 0644]
test/support/http_request_mock.ex
test/web/activity_pub/transmogrifier_test.exs
test/web/common_api/common_api_test.exs
test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
test/web/mastodon_api/controllers/account_controller_test.exs
test/web/mastodon_api/controllers/conversation_controller_test.exs
test/web/mastodon_api/controllers/status_controller_test.exs
test/web/mastodon_api/mastodon_api_controller_test.exs
test/web/mastodon_api/views/account_view_test.exs
test/web/pleroma_api/controllers/pleroma_api_controller_test.exs

index d007a69c3291334e8e976bfab8a2bcbe65c946fa..21b29752914e5d877c88336ba25cb5ec156ad45c 100644 (file)
@@ -56,6 +56,7 @@ Has these additional fields under the `pleroma` object:
 - `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
 - `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
 - `deactivated`: boolean, true when the user is deactivated
+- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
 
 ### Source
 
index be5821ad73c16b79c182c537c41fa9dea5fcc97b..098016af28c7b839bd6f019d4d61ea6e59ab3c7a 100644 (file)
@@ -67,6 +67,8 @@ defmodule Pleroma.Conversation do
 
       participations =
         Enum.map(users, fn user ->
+          User.increment_unread_conversation_count(conversation, user)
+
           {:ok, participation} =
             Participation.create_for_user_and_conversation(user, conversation, opts)
 
index e946f6de26eff61247f4618be47be3c5de14c72f..ab81f32173fd1a69694eed8aa39c1be0013843bc 100644 (file)
@@ -52,6 +52,15 @@ defmodule Pleroma.Conversation.Participation do
     participation
     |> read_cng(%{read: true})
     |> Repo.update()
+    |> case do
+      {:ok, participation} ->
+        participation = Repo.preload(participation, :user)
+        User.set_unread_conversation_count(participation.user)
+        {:ok, participation}
+
+      error ->
+        error
+    end
   end
 
   def mark_as_unread(participation) do
@@ -135,4 +144,12 @@ defmodule Pleroma.Conversation.Participation do
 
     {:ok, Repo.preload(participation, :recipients, force: true)}
   end
+
+  def unread_conversation_count_for_user(user) do
+    from(p in __MODULE__,
+      where: p.user_id == ^user.id,
+      where: not p.read,
+      select: %{count: count(p.id)}
+    )
+  end
 end
index c2f8fa0d77a7768290b2d7418d30efa648dba06d..0d665afa66338e74d900f0a2adfb5667e120ba53 100644 (file)
@@ -11,6 +11,7 @@ defmodule Pleroma.User do
   alias Comeonin.Pbkdf2
   alias Ecto.Multi
   alias Pleroma.Activity
+  alias Pleroma.Conversation.Participation
   alias Pleroma.Delivery
   alias Pleroma.Keys
   alias Pleroma.Notification
@@ -842,6 +843,61 @@ defmodule Pleroma.User do
 
   def maybe_update_following_count(user), do: user
 
+  def set_unread_conversation_count(%User{local: true} = user) do
+    unread_query = Participation.unread_conversation_count_for_user(user)
+
+    User
+    |> join(:inner, [u], p in subquery(unread_query))
+    |> update([u, p],
+      set: [
+        info:
+          fragment(
+            "jsonb_set(?, '{unread_conversation_count}', ?::varchar::jsonb, true)",
+            u.info,
+            p.count
+          )
+      ]
+    )
+    |> where([u], u.id == ^user.id)
+    |> select([u], u)
+    |> Repo.update_all([])
+    |> case do
+      {1, [user]} -> set_cache(user)
+      _ -> {:error, user}
+    end
+  end
+
+  def set_unread_conversation_count(_), do: :noop
+
+  def increment_unread_conversation_count(conversation, %User{local: true} = user) do
+    unread_query =
+      Participation.unread_conversation_count_for_user(user)
+      |> where([p], p.conversation_id == ^conversation.id)
+
+    User
+    |> join(:inner, [u], p in subquery(unread_query))
+    |> update([u, p],
+      set: [
+        info:
+          fragment(
+            "jsonb_set(?, '{unread_conversation_count}', (coalesce((?->>'unread_conversation_count')::int, 0) + 1)::varchar::jsonb, true)",
+            u.info,
+            u.info
+          )
+      ]
+    )
+    |> where([u], u.id == ^user.id)
+    |> where([u, p], p.count == 0)
+    |> select([u], u)
+    |> Repo.update_all([])
+    |> case do
+      {1, [user]} -> set_cache(user)
+      _ -> {:error, user}
+    end
+  end
+
+  def increment_unread_conversation_count(_, _), do: :noop
+
   def remove_duplicated_following(%User{following: following} = user) do
     uniq_following = Enum.uniq(following)
 
index ebd4ddebf2c68989ebb39b1b1aa3d5ad3fc982f3..4b5b43d7fc346fdb77fd67da66205d40b592745c 100644 (file)
@@ -47,6 +47,7 @@ defmodule Pleroma.User.Info do
     field(:hide_followers, :boolean, default: false)
     field(:hide_follows, :boolean, default: false)
     field(:hide_favorites, :boolean, default: true)
+    field(:unread_conversation_count, :integer, default: 0)
     field(:pinned_activities, {:array, :string}, default: [])
     field(:email_notifications, :map, default: %{"digest" => false})
     field(:mascot, :map, default: nil)
index 458d3590d8354d8f832636c80e99d721a93e5f21..e434c3b397eeeb66812475810d196b4ea17dbdd8 100644 (file)
@@ -17,6 +17,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.MRF
   alias Pleroma.Web.ActivityPub.Transmogrifier
+  alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.Streamer
   alias Pleroma.Web.WebFinger
   alias Pleroma.Workers.BackgroundWorker
@@ -291,8 +292,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   end
 
   def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
-    # only accept false as false value
     local = !(params[:local] == false)
+    activity_id = params[:activity_id]
 
     with data <- %{
            "to" => to,
@@ -301,6 +302,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
            "actor" => actor,
            "object" => object
          },
+         data <- Utils.maybe_put(data, "id", activity_id),
          {:ok, activity} <- insert(data, local),
          :ok <- maybe_federate(activity) do
       {:ok, activity}
index ea209b5ea1b73e81d23de17e4726fd422a420c30..05a03a37711d809c2063eaecd4e4dff9a0dcee74 100644 (file)
@@ -601,7 +601,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
       ) do
     with actor <- Containment.get_actor(data),
          {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
-         {:ok, object} <- get_obj_helper(object_id),
+         {:ok, object} <- get_embedded_obj_helper(object_id, actor),
          public <- Visibility.is_public?(data),
          {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do
       {:ok, activity}
@@ -642,7 +642,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
         to: data["to"] || [],
         cc: data["cc"] || [],
         object: object,
-        actor: actor_id
+        actor: actor_id,
+        activity_id: data["id"]
       })
     else
       e ->
@@ -824,6 +825,29 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     end
   end
 
+  @spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
+  def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
+        ap_id: ap_id
+      })
+      when attributed_to == ap_id do
+    with {:ok, activity} <-
+           handle_incoming(%{
+             "type" => "Create",
+             "to" => data["to"],
+             "cc" => data["cc"],
+             "actor" => attributed_to,
+             "object" => data
+           }) do
+      {:ok, Object.normalize(activity)}
+    else
+      _ -> get_obj_helper(object_id)
+    end
+  end
+
+  def get_embedded_obj_helper(object_id, _) do
+    get_obj_helper(object_id)
+  end
+
   def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
     with false <- String.starts_with?(in_reply_to, "http"),
          {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
index 3e43e8fb80621990f0b53220e490bf51191d588f..6263f206be6054239e635ce8a72a62a0e23f842b 100644 (file)
@@ -830,6 +830,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
     |> Repo.all()
   end
 
-  defp maybe_put(map, _key, nil), do: map
-  defp maybe_put(map, key, value), do: Map.put(map, key, value)
+  def maybe_put(map, _key, nil), do: map
+  def maybe_put(map, key, value), do: Map.put(map, key, value)
 end
index 995d4b1af96724ddeff1b36d31a28df374e48f26..caf9371de0481e697334ed3ce334f44432d9bd03 100644 (file)
@@ -16,6 +16,8 @@ defmodule Pleroma.Web.CommonAPI do
   import Pleroma.Web.Gettext
   import Pleroma.Web.CommonAPI.Utils
 
+  require Pleroma.Constants
+
   def follow(follower, followed) do
     timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
 
@@ -290,7 +292,7 @@ defmodule Pleroma.Web.CommonAPI do
 
     ActivityPub.update(%{
       local: true,
-      to: [user.follower_address],
+      to: [Pleroma.Constants.as_public(), user.follower_address],
       cc: [],
       actor: user.ap_id,
       object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
index df14ad66f9d426456cef27fd47e99157e0498eec..a56f0e149d1f146f8300f28cf80a21a29418d359 100644 (file)
@@ -105,6 +105,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
       |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
       |> Enum.dedup()
 
+    params =
+      if Map.has_key?(params, "fields_attributes") do
+        Map.update!(params, "fields_attributes", fn fields ->
+          fields
+          |> normalize_fields_attributes()
+          |> Enum.filter(fn %{"name" => n} -> n != "" end)
+        end)
+      else
+        params
+      end
+
     info_params =
       [
         :no_rich_text,
@@ -122,12 +133,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
         add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
       end)
       |> add_if_present(params, "default_scope", :default_scope)
-      |> add_if_present(params, "fields", :fields, fn fields ->
+      |> add_if_present(params, "fields_attributes", :fields, fn fields ->
         fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
 
         {:ok, fields}
       end)
-      |> add_if_present(params, "fields", :raw_fields)
+      |> add_if_present(params, "fields_attributes", :raw_fields)
       |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
         {:ok, Map.merge(user.info.pleroma_settings_store, value)}
       end)
@@ -168,6 +179,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
     end
   end
 
+  defp normalize_fields_attributes(fields) do
+    if Enum.all?(fields, &is_tuple/1) do
+      Enum.map(fields, fn {_, v} -> v end)
+    else
+      fields
+    end
+  end
+
   @doc "GET /api/v1/accounts/relationships"
   def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
     targets = User.get_all_by_ids(List.wrap(id))
@@ -301,4 +320,26 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
       {:error, message} -> json_response(conn, :forbidden, %{error: message})
     end
   end
+
+  @doc "POST /api/v1/follows"
+  def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
+    with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
+         {_, true} <- {:followed, follower.id != followed.id},
+         {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
+      render(conn, "show.json", user: followed, for: follower)
+    else
+      {:followed, _} -> {:error, :not_found}
+      {:error, message} -> json_response(conn, :forbidden, %{error: message})
+    end
+  end
+
+  @doc "GET /api/v1/mutes"
+  def mutes(%{assigns: %{user: user}} = conn, _) do
+    render(conn, "index.json", users: User.muted_users(user), for: user, as: :user)
+  end
+
+  @doc "GET /api/v1/blocks"
+  def blocks(%{assigns: %{user: user}} = conn, _) do
+    render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user)
+  end
 end
index e92f5d08935424f56826e280fa5fd5e601a944b4..7d839a8cf72918af7761d36586e03348683237c5 100644 (file)
@@ -5,86 +5,10 @@
 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
   use Pleroma.Web, :controller
 
-  import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
-
-  alias Pleroma.Bookmark
-  alias Pleroma.Pagination
-  alias Pleroma.User
-  alias Pleroma.Web.ActivityPub.ActivityPub
-  alias Pleroma.Web.CommonAPI
-  alias Pleroma.Web.MastodonAPI.AccountView
-  alias Pleroma.Web.MastodonAPI.StatusView
-
   require Logger
 
   action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
 
-  def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
-    with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
-         {_, true} <- {:followed, follower.id != followed.id},
-         {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
-      conn
-      |> put_view(AccountView)
-      |> render("show.json", %{user: followed, for: follower})
-    else
-      {:followed, _} ->
-        {:error, :not_found}
-
-      {:error, message} ->
-        conn
-        |> put_status(:forbidden)
-        |> json(%{error: message})
-    end
-  end
-
-  def mutes(%{assigns: %{user: user}} = conn, _) do
-    with muted_accounts <- User.muted_users(user) do
-      res = AccountView.render("index.json", users: muted_accounts, for: user, as: :user)
-      json(conn, res)
-    end
-  end
-
-  def blocks(%{assigns: %{user: user}} = conn, _) do
-    with blocked_accounts <- User.blocked_users(user) do
-      res = AccountView.render("index.json", users: blocked_accounts, for: user, as: :user)
-      json(conn, res)
-    end
-  end
-
-  def favourites(%{assigns: %{user: user}} = conn, params) do
-    params =
-      params
-      |> Map.put("type", "Create")
-      |> Map.put("favorited_by", user.ap_id)
-      |> Map.put("blocking_user", user)
-
-    activities =
-      ActivityPub.fetch_activities([], params)
-      |> Enum.reverse()
-
-    conn
-    |> add_link_headers(activities)
-    |> put_view(StatusView)
-    |> render("index.json", %{activities: activities, for: user, as: :activity})
-  end
-
-  def bookmarks(%{assigns: %{user: user}} = conn, params) do
-    user = User.get_cached_by_id(user.id)
-
-    bookmarks =
-      Bookmark.for_user_query(user.id)
-      |> Pagination.fetch_paginated(params)
-
-    activities =
-      bookmarks
-      |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
-
-    conn
-    |> add_link_headers(bookmarks)
-    |> put_view(StatusView)
-    |> render("index.json", %{activities: activities, for: user, as: :activity})
-  end
-
   # Stubs for unimplemented mastodon api
   #
   def empty_array(conn, _) do
index 79cced163dc4a98da36841968ee3ddedbc36197f..973334b60120783b30bfc52f9ca9f8ef6b9c8a4f 100644 (file)
@@ -5,7 +5,7 @@
 defmodule Pleroma.Web.MastodonAPI.StatusController do
   use Pleroma.Web, :controller
 
-  import Pleroma.Web.ControllerHelper, only: [try_render: 3]
+  import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2]
 
   require Ecto.Query
 
@@ -283,4 +283,39 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
       render(conn, "context.json", activity: activity, activities: activities, user: user)
     end
   end
+
+  @doc "GET /api/v1/favourites"
+  def favourites(%{assigns: %{user: user}} = conn, params) do
+    params =
+      params
+      |> Map.put("type", "Create")
+      |> Map.put("favorited_by", user.ap_id)
+      |> Map.put("blocking_user", user)
+
+    activities =
+      ActivityPub.fetch_activities([], params)
+      |> Enum.reverse()
+
+    conn
+    |> add_link_headers(activities)
+    |> render("index.json", activities: activities, for: user, as: :activity)
+  end
+
+  @doc "GET /api/v1/bookmarks"
+  def bookmarks(%{assigns: %{user: user}} = conn, params) do
+    user = User.get_cached_by_id(user.id)
+
+    bookmarks =
+      user.id
+      |> Bookmark.for_user_query()
+      |> Pleroma.Pagination.fetch_paginated(params)
+
+    activities =
+      bookmarks
+      |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
+
+    conn
+    |> add_link_headers(bookmarks)
+    |> render("index.json", %{activities: activities, for: user, as: :activity})
+  end
 end
index 99169ef95faecc4a2377e9ecac54259296d67d00..2d497689115c1cd85d990b95a9b00c0708464fe4 100644 (file)
@@ -167,6 +167,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
     |> maybe_put_chat_token(user, opts[:for], opts)
     |> maybe_put_activation_status(user, opts[:for])
     |> maybe_put_follow_requests_count(user, opts[:for])
+    |> maybe_put_unread_conversation_count(user, opts[:for])
   end
 
   defp username_from_nickname(string) when is_binary(string) do
@@ -248,6 +249,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
 
   defp maybe_put_activation_status(data, _, _), do: data
 
+  defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{id: user_id}) do
+    data
+    |> Kernel.put_in(
+      [:pleroma, :unread_conversation_count],
+      user.info.unread_conversation_count
+    )
+  end
+
+  defp maybe_put_unread_conversation_count(data, _, _), do: data
+
   defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
   defp image_url(_), do: nil
 end
index 305987d3f4f99596ec7a39f0df6af088fff1d081..16cf234990937426b771965151284179dfd2919a 100644 (file)
@@ -355,14 +355,14 @@ defmodule Pleroma.Web.Router do
       get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
 
       get("/follow_requests", FollowRequestController, :index)
-      get("/blocks", MastodonAPIController, :blocks)
-      get("/mutes", MastodonAPIController, :mutes)
+      get("/blocks", AccountController, :blocks)
+      get("/mutes", AccountController, :mutes)
 
       get("/timelines/home", TimelineController, :home)
       get("/timelines/direct", TimelineController, :direct)
 
-      get("/favourites", MastodonAPIController, :favourites)
-      get("/bookmarks", MastodonAPIController, :bookmarks)
+      get("/favourites", StatusController, :favourites)
+      get("/bookmarks", StatusController, :bookmarks)
 
       get("/notifications", NotificationController, :index)
       get("/notifications/:id", NotificationController, :show)
@@ -434,7 +434,7 @@ defmodule Pleroma.Web.Router do
     scope [] do
       pipe_through(:oauth_follow)
 
-      post("/follows", MastodonAPIController, :follows)
+      post("/follows", AccountController, :follows)
       post("/accounts/:id/follow", AccountController, :follow)
       post("/accounts/:id/unfollow", AccountController, :unfollow)
       post("/accounts/:id/block", AccountController, :block)
diff --git a/priv/repo/migrations/20191005165212_add_unread_conversation_count_to_user_info.exs b/priv/repo/migrations/20191005165212_add_unread_conversation_count_to_user_info.exs
new file mode 100644 (file)
index 0000000..2aa1a01
--- /dev/null
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.AddUnreadConversationCountToUserInfo do
+  use Ecto.Migration
+
+  def up do
+    execute("""
+    update users set info = jsonb_set(info, '{unread_conversation_count}', 0::varchar::jsonb, true) where local=true
+    """)
+  end
+
+  def down, do: :ok
+end
index a27167d4299b2b1c05c77e1c88c9541d75fdbcb6..f430bdf75f5def543af0eab7a9cf9cceb4ae5cbf 100644 (file)
@@ -6,6 +6,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
   use Pleroma.DataCase
   import Pleroma.Factory
   alias Pleroma.Conversation.Participation
+  alias Pleroma.User
   alias Pleroma.Web.CommonAPI
 
   test "getting a participation will also preload things" do
@@ -30,6 +31,8 @@ defmodule Pleroma.Conversation.ParticipationTest do
     {:ok, activity} =
       CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
 
+    user = User.get_cached_by_id(user.id)
+    other_user = User.get_cached_by_id(user.id)
     [participation] = Participation.for_user(user)
     participation = Pleroma.Repo.preload(participation, :recipients)
 
@@ -155,6 +158,7 @@ defmodule Pleroma.Conversation.ParticipationTest do
     [participation] = Participation.for_user_with_last_activity_id(user)
 
     participation = Repo.preload(participation, :recipients)
+    user = User.get_cached_by_id(user.id)
 
     assert participation.recipients |> length() == 1
     assert user in participation.recipients
diff --git a/test/fixtures/bogus-mastodon-announce.json b/test/fixtures/bogus-mastodon-announce.json
new file mode 100644 (file)
index 0000000..0485b80
--- /dev/null
@@ -0,0 +1,43 @@
+{
+  "type": "Announce",
+  "to": [
+    "https://www.w3.org/ns/activitystreams#Public"
+  ],
+  "published": "2018-02-17T19:39:15Z",
+  "object": {
+    "type": "Note",
+    "id": "https://mastodon.social/users/emelie/statuses/101849165031453404",
+    "attributedTo": "https://mastodon.social/users/emelie",
+    "content": "this is a public toot",
+    "to": [
+      "https://www.w3.org/ns/activitystreams#Public"
+    ],
+    "cc": [
+      "https://mastodon.social/users/emelie",
+      "https://mastodon.social/users/emelie/followers"
+    ]
+  },
+  "id": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
+  "cc": [
+    "http://mastodon.example.org/users/admin",
+    "http://mastodon.example.org/users/admin/followers"
+  ],
+  "atomUri": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
+  "actor": "http://mastodon.example.org/users/admin",
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "toot": "http://joinmastodon.org/ns#",
+      "sensitive": "as:sensitive",
+      "ostatus": "http://ostatus.org#",
+      "movedTo": "as:movedTo",
+      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+      "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+      "conversation": "ostatus:conversation",
+      "atomUri": "ostatus:atomUri",
+      "Hashtag": "as:Hashtag",
+      "Emoji": "toot:Emoji"
+    }
+  ]
+}
diff --git a/test/fixtures/mastodon-announce-private.json b/test/fixtures/mastodon-announce-private.json
new file mode 100644 (file)
index 0000000..9b868b1
--- /dev/null
@@ -0,0 +1,35 @@
+{
+  "type": "Announce",
+  "to": [
+    "http://mastodon.example.org/users/admin/followers"
+  ],
+  "published": "2018-02-17T19:39:15Z",
+  "object": {
+    "type": "Note",
+    "id": "http://mastodon.example.org/@admin/99541947525187368",
+    "attributedTo": "http://mastodon.example.org/users/admin",
+    "content": "this is a private toot",
+    "to": [
+      "http://mastodon.example.org/users/admin/followers"
+    ]
+  },
+  "id": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
+  "atomUri": "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity",
+  "actor": "http://mastodon.example.org/users/admin",
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "toot": "http://joinmastodon.org/ns#",
+      "sensitive": "as:sensitive",
+      "ostatus": "http://ostatus.org#",
+      "movedTo": "as:movedTo",
+      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+      "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+      "conversation": "ostatus:conversation",
+      "atomUri": "ostatus:atomUri",
+      "Hashtag": "as:Hashtag",
+      "Emoji": "toot:Emoji"
+    }
+  ]
+}
index 5506c06268dbe396c3d427ea19eaec265cdec1ab..b825a93075b7d856fd8391dac67e26ef86a5aa52 100644 (file)
@@ -46,6 +46,14 @@ defmodule HttpRequestMock do
      }}
   end
 
+  def get("https://mastodon.social/users/emelie/statuses/101849165031453404", _, _, _) do
+    {:ok,
+     %Tesla.Env{
+       status: 404,
+       body: ""
+     }}
+  end
+
   def get("https://mastodon.social/users/emelie", _, _, _) do
     {:ok,
      %Tesla.Env{
@@ -349,6 +357,14 @@ defmodule HttpRequestMock do
      }}
   end
 
+  def get("http://mastodon.example.org/@admin/99541947525187368", _, _, _) do
+    {:ok,
+     %Tesla.Env{
+       status: 404,
+       body: ""
+     }}
+  end
+
   def get("https://shitposter.club/notice/7369654", _, _, _) do
     {:ok,
      %Tesla.Env{
index a9544caf2a98065ff08f360e503fc43b4a70e319..c8e205afc65b2311829f73d00291c3b49f911d9b 100644 (file)
@@ -479,6 +479,33 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert Activity.get_create_by_object_ap_id(data["object"]).id == activity.id
     end
 
+    test "it works for incoming announces with an inlined activity" do
+      data =
+        File.read!("test/fixtures/mastodon-announce-private.json")
+        |> Poison.decode!()
+
+      {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
+
+      assert data["actor"] == "http://mastodon.example.org/users/admin"
+      assert data["type"] == "Announce"
+
+      assert data["id"] ==
+               "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
+
+      object = Object.normalize(data["object"])
+
+      assert object.data["id"] == "http://mastodon.example.org/@admin/99541947525187368"
+      assert object.data["content"] == "this is a private toot"
+    end
+
+    test "it rejects incoming announces with an inlined activity from another origin" do
+      data =
+        File.read!("test/fixtures/bogus-mastodon-announce.json")
+        |> Poison.decode!()
+
+      assert :error = Transmogrifier.handle_incoming(data)
+    end
+
     test "it does not clobber the addressing on announce activities" do
       user = insert(:user)
       {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
@@ -597,6 +624,8 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
 
       {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
 
+      assert data["id"] == update_data["id"]
+
       user = User.get_cached_by_ap_id(data["actor"])
       assert user.name == "gargle"
 
index b9e785885053363e3b2fd4e46899c149fe449f70..cc1d122d9ee7610a08ce7ba303d46fb83b4a44fd 100644 (file)
@@ -14,6 +14,8 @@ defmodule Pleroma.Web.CommonAPITest do
 
   import Pleroma.Factory
 
+  require Pleroma.Constants
+
   clear_config([:instance, :safe_dm_mentions])
   clear_config([:instance, :limit])
   clear_config([:instance, :max_pinned_statuses])
@@ -96,11 +98,13 @@ defmodule Pleroma.Web.CommonAPITest do
   test "it adds emoji when updating profiles" do
     user = insert(:user, %{name: ":firefox:"})
 
-    CommonAPI.update(user)
+    {:ok, activity} = CommonAPI.update(user)
     user = User.get_cached_by_ap_id(user.ap_id)
     [firefox] = user.info.source_data["tag"]
 
     assert firefox["name"] == ":firefox:"
+
+    assert Pleroma.Constants.as_public() in activity.recipients
   end
 
   describe "posting" do
index 560f5513757c2831b87260ed09088ae4a6804d09..599cd61c811cae81dbf8eee80512de389b88022d 100644 (file)
@@ -328,7 +328,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       account =
         conn
         |> assign(:user, user)
-        |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields})
+        |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
         |> json_response(200)
 
       assert account["fields"] == [
@@ -344,6 +344,35 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
                %{"name" => "link", "value" => "cofe.io"}
              ]
 
+      fields =
+        [
+          "fields_attributes[1][name]=link",
+          "fields_attributes[1][value]=cofe.io",
+          "fields_attributes[0][name]=<a href=\"http://google.com\">foo</a>",
+          "fields_attributes[0][value]=bar"
+        ]
+        |> Enum.join("&")
+
+      account =
+        conn
+        |> put_req_header("content-type", "application/x-www-form-urlencoded")
+        |> assign(:user, user)
+        |> patch("/api/v1/accounts/update_credentials", fields)
+        |> json_response(200)
+
+      assert account["fields"] == [
+               %{"name" => "foo", "value" => "bar"},
+               %{"name" => "link", "value" => ~S(<a href="http://cofe.io" rel="ugc">cofe.io</a>)}
+             ]
+
+      assert account["source"]["fields"] == [
+               %{
+                 "name" => "<a href=\"http://google.com\">foo</a>",
+                 "value" => "bar"
+               },
+               %{"name" => "link", "value" => "cofe.io"}
+             ]
+
       name_limit = Pleroma.Config.get([:instance, :account_field_name_length])
       value_limit = Pleroma.Config.get([:instance, :account_field_value_length])
 
@@ -354,7 +383,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       assert %{"error" => "Invalid request"} ==
                conn
                |> assign(:user, user)
-               |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields})
+               |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
                |> json_response(403)
 
       long_name = Enum.map(0..name_limit, fn _ -> "x" end) |> Enum.join()
@@ -364,7 +393,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       assert %{"error" => "Invalid request"} ==
                conn
                |> assign(:user, user)
-               |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields})
+               |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
                |> json_response(403)
 
       Pleroma.Config.put([:instance, :max_account_fields], 1)
@@ -377,8 +406,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       assert %{"error" => "Invalid request"} ==
                conn
                |> assign(:user, user)
-               |> patch("/api/v1/accounts/update_credentials", %{"fields" => fields})
+               |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
                |> json_response(403)
+
+      fields = [
+        %{"name" => "foo", "value" => ""},
+        %{"name" => "", "value" => "bar"}
+      ]
+
+      account =
+        conn
+        |> assign(:user, user)
+        |> patch("/api/v1/accounts/update_credentials", %{"fields_attributes" => fields})
+        |> json_response(200)
+
+      assert account["fields"] == [
+               %{"name" => "foo", "value" => ""}
+             ]
     end
   end
 end
index 8c8017838ceaecbdc63d34ffe21693bdb9d15559..6a59c3d947f45983920fb25455ae3c09359fbf0f 100644 (file)
@@ -849,4 +849,34 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
       assert [] = json_response(conn, 200)
     end
   end
+
+  test "getting a list of mutes", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, user} = User.mute(user, other_user)
+
+    conn =
+      conn
+      |> assign(:user, user)
+      |> get("/api/v1/mutes")
+
+    other_user_id = to_string(other_user.id)
+    assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
+  end
+
+  test "getting a list of blocks", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, user} = User.block(user, other_user)
+
+    conn =
+      conn
+      |> assign(:user, user)
+      |> get("/api/v1/blocks")
+
+    other_user_id = to_string(other_user.id)
+    assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
+  end
 end
index 7117fc76a3d7399ee01f4a7bba891eb6c077b9b3..a308a76201e216edb106cb92fb7b4ee971f41f61 100644 (file)
@@ -10,19 +10,23 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
 
   import Pleroma.Factory
 
-  test "Conversations", %{conn: conn} do
+  test "returns a list of conversations", %{conn: conn} do
     user_one = insert(:user)
     user_two = insert(:user)
     user_three = insert(:user)
 
     {:ok, user_two} = User.follow(user_two, user_one)
 
+    assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0
+
     {:ok, direct} =
       CommonAPI.post(user_one, %{
         "status" => "Hi @#{user_two.nickname}, @#{user_three.nickname}!",
         "visibility" => "direct"
       })
 
+    assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 1
+
     {:ok, _follower_only} =
       CommonAPI.post(user_one, %{
         "status" => "Hi @#{user_two.nickname}!",
@@ -52,23 +56,100 @@ defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
     assert is_binary(res_id)
     assert unread == true
     assert res_last_status["id"] == direct.id
+    assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
+  end
 
-    # Apparently undocumented API endpoint
-    res_conn =
+  test "updates the last_status on reply", %{conn: conn} do
+    user_one = insert(:user)
+    user_two = insert(:user)
+
+    {:ok, direct} =
+      CommonAPI.post(user_one, %{
+        "status" => "Hi @#{user_two.nickname}",
+        "visibility" => "direct"
+      })
+
+    {:ok, direct_reply} =
+      CommonAPI.post(user_two, %{
+        "status" => "reply",
+        "visibility" => "direct",
+        "in_reply_to_status_id" => direct.id
+      })
+
+    [%{"last_status" => res_last_status}] =
       conn
       |> assign(:user, user_one)
-      |> post("/api/v1/conversations/#{res_id}/read")
+      |> get("/api/v1/conversations")
+      |> json_response(200)
 
-    assert response = json_response(res_conn, 200)
-    assert length(response["accounts"]) == 2
-    assert response["last_status"]["id"] == direct.id
-    assert response["unread"] == false
+    assert res_last_status["id"] == direct_reply.id
+  end
+
+  test "the user marks a conversation as read", %{conn: conn} do
+    user_one = insert(:user)
+    user_two = insert(:user)
+
+    {:ok, direct} =
+      CommonAPI.post(user_one, %{
+        "status" => "Hi @#{user_two.nickname}",
+        "visibility" => "direct"
+      })
+
+    [%{"id" => direct_conversation_id, "unread" => true}] =
+      conn
+      |> assign(:user, user_one)
+      |> get("/api/v1/conversations")
+      |> json_response(200)
+
+    %{"unread" => false} =
+      conn
+      |> assign(:user, user_one)
+      |> post("/api/v1/conversations/#{direct_conversation_id}/read")
+      |> json_response(200)
+
+    assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0
+
+    # The conversation is marked as unread on reply
+    {:ok, _} =
+      CommonAPI.post(user_two, %{
+        "status" => "reply",
+        "visibility" => "direct",
+        "in_reply_to_status_id" => direct.id
+      })
+
+    [%{"unread" => true}] =
+      conn
+      |> assign(:user, user_one)
+      |> get("/api/v1/conversations")
+      |> json_response(200)
+
+    assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
+
+    # A reply doesn't increment the user's unread_conversation_count if the conversation is unread
+    {:ok, _} =
+      CommonAPI.post(user_two, %{
+        "status" => "reply",
+        "visibility" => "direct",
+        "in_reply_to_status_id" => direct.id
+      })
+
+    assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
+  end
+
+  test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do
+    user_one = insert(:user)
+    user_two = insert(:user)
+
+    {:ok, direct} =
+      CommonAPI.post(user_one, %{
+        "status" => "Hi @#{user_two.nickname}!",
+        "visibility" => "direct"
+      })
 
-    # (vanilla) Mastodon frontend behaviour
     res_conn =
       conn
       |> assign(:user, user_one)
-      |> get("/api/v1/statuses/#{res_last_status["id"]}/context")
+      |> get("/api/v1/statuses/#{direct.id}/context")
 
     assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200)
   end
index b648ad6ff4547ecdf56bd47e0d7dab1eb5d34ead..a4bbfe0557bc2b7ff22414e78343b3cd51e4abdf 100644 (file)
@@ -1242,4 +1242,51 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
              "descendants" => [%{"id" => ^id4}, %{"id" => ^id5}]
            } = response
   end
+
+  test "returns the favorites of a user", %{conn: conn} do
+    user = insert(:user)
+    other_user = insert(:user)
+
+    {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
+    {:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
+
+    {:ok, _, _} = CommonAPI.favorite(activity.id, user)
+
+    first_conn =
+      conn
+      |> assign(:user, user)
+      |> get("/api/v1/favourites")
+
+    assert [status] = json_response(first_conn, 200)
+    assert status["id"] == to_string(activity.id)
+
+    assert [{"link", _link_header}] =
+             Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
+
+    # Honours query params
+    {:ok, second_activity} =
+      CommonAPI.post(other_user, %{
+        "status" =>
+          "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
+      })
+
+    {:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
+
+    last_like = status["id"]
+
+    second_conn =
+      conn
+      |> assign(:user, user)
+      |> get("/api/v1/favourites?since_id=#{last_like}")
+
+    assert [second_status] = json_response(second_conn, 200)
+    assert second_status["id"] == to_string(second_activity.id)
+
+    third_conn =
+      conn
+      |> assign(:user, user)
+      |> get("/api/v1/favourites?limit=0")
+
+    assert [] = json_response(third_conn, 200)
+  end
 end
index c03003dac461c0f9123868179ce7f6483a7a2346..42a8779c00edc291b7e05d170f11b5e4471452a3 100644 (file)
@@ -7,7 +7,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
   alias Pleroma.Notification
   alias Pleroma.Repo
-  alias Pleroma.User
   alias Pleroma.Web.CommonAPI
 
   import Pleroma.Factory
@@ -20,36 +19,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
   clear_config([:rich_media, :enabled])
 
-  test "getting a list of mutes", %{conn: conn} do
-    user = insert(:user)
-    other_user = insert(:user)
-
-    {:ok, user} = User.mute(user, other_user)
-
-    conn =
-      conn
-      |> assign(:user, user)
-      |> get("/api/v1/mutes")
-
-    other_user_id = to_string(other_user.id)
-    assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
-  end
-
-  test "getting a list of blocks", %{conn: conn} do
-    user = insert(:user)
-    other_user = insert(:user)
-
-    {:ok, user} = User.block(user, other_user)
-
-    conn =
-      conn
-      |> assign(:user, user)
-      |> get("/api/v1/blocks")
-
-    other_user_id = to_string(other_user.id)
-    assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
-  end
-
   test "unimplemented follow_requests, blocks, domain blocks" do
     user = insert(:user)
 
@@ -64,53 +33,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     end)
   end
 
-  test "returns the favorites of a user", %{conn: conn} do
-    user = insert(:user)
-    other_user = insert(:user)
-
-    {:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
-    {:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
-
-    {:ok, _, _} = CommonAPI.favorite(activity.id, user)
-
-    first_conn =
-      conn
-      |> assign(:user, user)
-      |> get("/api/v1/favourites")
-
-    assert [status] = json_response(first_conn, 200)
-    assert status["id"] == to_string(activity.id)
-
-    assert [{"link", _link_header}] =
-             Enum.filter(first_conn.resp_headers, fn element -> match?({"link", _}, element) end)
-
-    # Honours query params
-    {:ok, second_activity} =
-      CommonAPI.post(other_user, %{
-        "status" =>
-          "Trees Are Never Sad Look At Them Every Once In Awhile They're Quite Beautiful."
-      })
-
-    {:ok, _, _} = CommonAPI.favorite(second_activity.id, user)
-
-    last_like = status["id"]
-
-    second_conn =
-      conn
-      |> assign(:user, user)
-      |> get("/api/v1/favourites?since_id=#{last_like}")
-
-    assert [second_status] = json_response(second_conn, 200)
-    assert second_status["id"] == to_string(second_activity.id)
-
-    third_conn =
-      conn
-      |> assign(:user, user)
-      |> get("/api/v1/favourites?limit=0")
-
-    assert [] = json_response(third_conn, 200)
-  end
-
   describe "link headers" do
     test "preserves parameters in link headers", %{conn: conn} do
       user = insert(:user)
index 62b2ab7e36eb74a06de9df3bdd6416416ff392e7..b7a4938a65996df0ea312bdd9381817aed0dc7b2 100644 (file)
@@ -418,6 +418,27 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
                following_count: 1
              } = AccountView.render("show.json", %{user: user, for: user})
     end
+
+    test "shows unread_conversation_count only to the account owner" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, _activity} =
+        CommonAPI.post(user, %{
+          "status" => "Hey @#{other_user.nickname}.",
+          "visibility" => "direct"
+        })
+
+      user = User.get_cached_by_ap_id(user.ap_id)
+
+      assert AccountView.render("show.json", %{user: user, for: other_user})[:pleroma][
+               :unread_conversation_count
+             ] == nil
+
+      assert AccountView.render("show.json", %{user: user, for: user})[:pleroma][
+               :unread_conversation_count
+             ] == 1
+    end
   end
 
   describe "follow requests counter" do
index 3a5dbdeead36b91d250983c2fc7a1f21c970a8df..44ea85b453aa34a96cd0ac9f7d33d19322824d96 100644 (file)
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
   alias Pleroma.Notification
   alias Pleroma.Object
   alias Pleroma.Repo
+  alias Pleroma.User
   alias Pleroma.Web.CommonAPI
 
   import Pleroma.Factory
@@ -133,6 +134,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
 
     participation = Repo.preload(participation, :recipients)
 
+    user = User.get_cached_by_id(user.id)
     assert [user] == participation.recipients
     assert other_user not in participation.recipients