diff options
| -rw-r--r-- | lib/pleroma/plugs/http_signature.ex | 2 | ||||
| -rw-r--r-- | lib/pleroma/user.ex | 32 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 15 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/transmogrifier.ex | 82 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/utils.ex | 13 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/views/user_view.ex | 2 | ||||
| -rw-r--r-- | lib/pleroma/web/http_signatures/http_signatures.ex | 4 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 4 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/views/account_view.ex | 2 | ||||
| -rw-r--r-- | lib/pleroma/web/twitter_api/twitter_api.ex | 2 | ||||
| -rw-r--r-- | test/fixtures/mastodon-reject-activity.json | 34 | ||||
| -rw-r--r-- | test/web/activity_pub/transmogrifier_test.exs | 163 | ||||
| -rw-r--r-- | test/web/mastodon_api/mastodon_api_controller_test.exs | 2 | 
13 files changed, 327 insertions, 30 deletions
diff --git a/lib/pleroma/plugs/http_signature.ex b/lib/pleroma/plugs/http_signature.ex index 2d0e10cad..38bcd3a78 100644 --- a/lib/pleroma/plugs/http_signature.ex +++ b/lib/pleroma/plugs/http_signature.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do    end    def call(conn, _opts) do -    user = Utils.normalize_actor(conn.params["actor"]) +    user = Utils.get_ap_id(conn.params["actor"])      Logger.debug("Checking sig for #{user}")      [signature | _] = get_req_header(conn, "signature") diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 508f14584..75e173d0c 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -67,7 +67,8 @@ defmodule Pleroma.User do      %{        following_count: length(user.following) - oneself,        note_count: user.info["note_count"] || 0, -      follower_count: user.info["follower_count"] || 0 +      follower_count: user.info["follower_count"] || 0, +      locked: user.info["locked"] || false      }    end @@ -167,6 +168,35 @@ defmodule Pleroma.User do      end    end +  def maybe_direct_follow(%User{} = follower, %User{info: info} = followed) do +    user_info = user_info(followed) + +    should_direct_follow = +      cond do +        # if the account is locked, don't pre-create the relationship +        user_info.locked == true -> +          false + +        # if the users are blocking each other, we shouldn't even be here, but check for it anyway +        User.blocks?(follower, followed) == true or User.blocks?(followed, follower) == true -> +          false + +        # if OStatus, then there is no three-way handshake to follow +        User.ap_enabled?(followed) != true -> +          true + +        # if there are no other reasons not to, just pre-create the relationship +        true -> +          true +      end + +    if should_direct_follow do +      follow(follower, followed) +    else +      follower +    end +  end +    def follow(%User{} = follower, %User{info: info} = followed) do      ap_followers = followed.follower_address diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 8485a8009..1a1bfbffd 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -95,6 +95,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  def reject(%{to: to, actor: actor, object: object} = params) do +    # only accept false as false value +    local = !(params[:local] == false) + +    with data <- %{"to" => to, "type" => "Reject", "actor" => actor, "object" => object}, +         {:ok, activity} <- insert(data, local), +         :ok <- maybe_federate(activity) do +      {:ok, activity} +    end +  end +    def update(%{to: to, cc: cc, actor: actor, object: object} = params) do      # only accept false as false value      local = !(params[:local] == false) @@ -464,6 +475,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do            "url" => [%{"href" => data["image"]["url"]}]          } +    locked = data["manuallyApprovesFollowers"] || false      data = Transmogrifier.maybe_fix_user_object(data)      user_data = %{ @@ -471,7 +483,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        info: %{          "ap_enabled" => true,          "source_data" => data, -        "banner" => banner +        "banner" => banner, +        "locked" => locked        },        avatar: avatar,        nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}", diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index eada4334e..3c9377be9 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    alias Pleroma.Activity    alias Pleroma.Repo    alias Pleroma.Web.ActivityPub.ActivityPub +  alias Pleroma.Web.ActivityPub.Utils    import Ecto.Query @@ -145,6 +146,78 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      end    end +  defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do +    with true <- id =~ "follows", +         %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), +         %Activity{} = activity <- Utils.fetch_latest_follow(follower, followed) do +      {:ok, activity} +    else +      _ -> {:error, nil} +    end +  end + +  defp mastodon_follow_hack(_), do: {:error, nil} + +  defp get_follow_activity(follow_object, followed) do +    with object_id when not is_nil(object_id) <- Utils.get_ap_id(follow_object), +         {_, %Activity{} = activity} <- {:activity, Activity.get_by_ap_id(object_id)} do +      {:ok, activity} +    else +      # Can't find the activity. This might a Mastodon 2.3 "Accept" +      {:activity, nil} -> +        mastodon_follow_hack(follow_object, followed) + +      _ -> +        {:error, nil} +    end +  end + +  def handle_incoming( +        %{"type" => "Accept", "object" => follow_object, "actor" => actor, "id" => id} = data +      ) do +    with %User{} = followed <- User.get_or_fetch_by_ap_id(actor), +         {:ok, follow_activity} <- get_follow_activity(follow_object, followed), +         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), +         {:ok, activity} <- +           ActivityPub.accept(%{ +             to: follow_activity.data["to"], +             type: "Accept", +             actor: followed.ap_id, +             object: follow_activity.data["id"], +             local: false +           }) do +      if not User.following?(follower, followed) do +        {:ok, follower} = User.follow(follower, followed) +      end + +      {:ok, activity} +    else +      _e -> :error +    end +  end + +  def handle_incoming( +        %{"type" => "Reject", "object" => follow_object, "actor" => actor, "id" => id} = data +      ) do +    with %User{} = followed <- User.get_or_fetch_by_ap_id(actor), +         {:ok, follow_activity} <- get_follow_activity(follow_object, followed), +         %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), +         {:ok, activity} <- +           ActivityPub.accept(%{ +             to: follow_activity.data["to"], +             type: "Accept", +             actor: followed.ap_id, +             object: follow_activity.data["id"], +             local: false +           }) do +      User.unfollow(follower, followed) + +      {:ok, activity} +    else +      _e -> :error +    end +  end +    def handle_incoming(          %{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = _data        ) do @@ -207,11 +280,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def handle_incoming(          %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = _data        ) do -    object_id = -      case object_id do -        %{"id" => id} -> id -        id -> id -      end +    object_id = Utils.get_ap_id(object_id)      with %User{} = _actor <- User.get_or_fetch_by_ap_id(actor),           {:ok, object} <- @@ -314,9 +383,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      end    end -  # TODO -  # Accept -    def handle_incoming(_), do: :error    def get_obj_helper(id) do diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index cb2e1e078..56b80a8db 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -7,18 +7,15 @@ defmodule Pleroma.Web.ActivityPub.Utils do    # Some implementations send the actor URI as the actor field, others send the entire actor object,    # so figure out what the actor's URI is based on what we have. -  def normalize_actor(actor) do -    cond do -      is_binary(actor) -> -        actor - -      is_map(actor) -> -        actor["id"] +  def get_ap_id(object) do +    case object do +      %{"id" => id} -> id +      id -> id      end    end    def normalize_params(params) do -    Map.put(params, "actor", normalize_actor(params["actor"])) +    Map.put(params, "actor", get_ap_id(params["actor"]))    end    def make_json_ld_header do diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index ffd76b529..f4b2e0610 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do        "name" => user.name,        "summary" => user.bio,        "url" => user.ap_id, -      "manuallyApprovesFollowers" => false, +      "manuallyApprovesFollowers" => user.info["locked"] || false,        "publicKey" => %{          "id" => "#{user.ap_id}#main-key",          "owner" => user.ap_id, diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex index 4e0adbc1d..5e42a871b 100644 --- a/lib/pleroma/web/http_signatures/http_signatures.ex +++ b/lib/pleroma/web/http_signatures/http_signatures.ex @@ -32,14 +32,14 @@ defmodule Pleroma.Web.HTTPSignatures do    def validate_conn(conn) do      # TODO: How to get the right key and see if it is actually valid for that request.      # For now, fetch the key for the actor. -    with actor_id <- Utils.normalize_actor(conn.params["actor"]), +    with actor_id <- Utils.get_ap_id(conn.params["actor"]),           {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do        if validate_conn(conn, public_key) do          true        else          Logger.debug("Could not validate, re-fetching user and trying one more time")          # Fetch user anew and try one more time -        with actor_id <- Utils.normalize_actor(conn.params["actor"]), +        with actor_id <- Utils.get_ap_id(conn.params["actor"]),               {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id),               {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do            validate_conn(conn, public_key) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 460942f1a..e12d3fb5b 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -429,7 +429,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    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 @@ -442,7 +442,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 diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index f378bb36e..9db683f44 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do        username: hd(String.split(user.nickname, "@")),        acct: user.nickname,        display_name: user.name || user.nickname, -      locked: false, +      locked: user_info.locked,        created_at: Utils.to_masto_date(user.inserted_at),        followers_count: user_info.follower_count,        following_count: user_info.following_count, diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 3ccdaed6f..331efa90b 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -25,7 +25,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do    def follow(%User{} = follower, params) do      with {:ok, %User{} = followed} <- get_user(params), -         {:ok, follower} <- User.follow(follower, followed), +         {:ok, follower} <- User.maybe_direct_follow(follower, followed),           {:ok, activity} <- ActivityPub.follow(follower, followed) do        {:ok, follower, followed, activity}      else diff --git a/test/fixtures/mastodon-reject-activity.json b/test/fixtures/mastodon-reject-activity.json new file mode 100644 index 000000000..9559d6c73 --- /dev/null +++ b/test/fixtures/mastodon-reject-activity.json @@ -0,0 +1,34 @@ +{ +  "type": "Reject", +  "signature": { +    "type": "RsaSignature2017", +    "signatureValue": "rBzK4Kqhd4g7HDS8WE5oRbWQb2R+HF/6awbUuMWhgru/xCODT0SJWSri0qWqEO4fPcpoUyz2d25cw6o+iy9wiozQb3hQNnu69AR+H5Mytc06+g10KCHexbGhbAEAw/7IzmeXELHUbaqeduaDIbdt1zw4RkwLXdqgQcGXTJ6ND1wM3WMHXQCK1m0flasIXFoBxpliPAGiElV8s0+Ltuh562GvflG3kB3WO+j+NaR0ZfG5G9N88xMj9UQlCKit5gpAE5p6syUsCU2WGBHywTumv73i3OVTIFfq+P9AdMsRuzw1r7zoKEsthW4aOzLQDi01ZjvdBz8zH6JnjDU7SMN/Ig==", +    "creator": "http://mastodon.example.org/users/admin#main-key", +    "created": "2018-02-17T14:36:41Z" +  }, +  "object": { +    "type": "Follow", +    "object": "http://mastodon.example.org/users/admin", +    "id": "http://localtesting.pleroma.lol/users/lain#follows/4", +    "actor": "http://localtesting.pleroma.lol/users/lain" +  }, +  "nickname": "lain", +  "id": "http://mastodon.example.org/users/admin#rejects/follows/4", +  "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/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index cf6b1d0b5..384844095 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -2,13 +2,12 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do    use Pleroma.DataCase    alias Pleroma.Web.ActivityPub.Transmogrifier    alias Pleroma.Web.ActivityPub.Utils +  alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.OStatus    alias Pleroma.Activity    alias Pleroma.User    alias Pleroma.Repo    alias Pleroma.Web.Websub.WebsubClientSubscription -  alias Pleroma.Web.Websub.WebsubServerSubscription -  import Ecto.Query    import Pleroma.Factory    alias Pleroma.Web.CommonAPI @@ -283,7 +282,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do          |> Map.put("object", object)          |> Map.put("actor", activity.data["actor"]) -      {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) +      {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)        refute Repo.get(Activity, activity.id)      end @@ -385,6 +384,164 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do        refute User.blocks?(blocker, user)      end + +    test "it works for incoming accepts which were pre-accepted" do +      follower = insert(:user) +      followed = insert(:user) + +      {:ok, follower} = User.follow(follower, followed) +      assert User.following?(follower, followed) == true + +      {:ok, follow_activity} = ActivityPub.follow(follower, followed) + +      accept_data = +        File.read!("test/fixtures/mastodon-accept-activity.json") +        |> Poison.decode!() +        |> Map.put("actor", followed.ap_id) + +      object = +        accept_data["object"] +        |> Map.put("actor", follower.ap_id) +        |> Map.put("id", follow_activity.data["id"]) + +      accept_data = Map.put(accept_data, "object", object) + +      {:ok, activity} = Transmogrifier.handle_incoming(accept_data) +      refute activity.local + +      assert activity.data["object"] == follow_activity.data["id"] + +      follower = Repo.get(User, follower.id) + +      assert User.following?(follower, followed) == true +    end + +    test "it works for incoming accepts which were orphaned" do +      follower = insert(:user) +      followed = insert(:user, %{info: %{"locked" => true}}) + +      {:ok, follow_activity} = ActivityPub.follow(follower, followed) + +      accept_data = +        File.read!("test/fixtures/mastodon-accept-activity.json") +        |> Poison.decode!() +        |> Map.put("actor", followed.ap_id) + +      accept_data = +        Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) + +      {:ok, activity} = Transmogrifier.handle_incoming(accept_data) +      assert activity.data["object"] == follow_activity.data["id"] + +      follower = Repo.get(User, follower.id) + +      assert User.following?(follower, followed) == true +    end + +    test "it works for incoming accepts which are referenced by IRI only" do +      follower = insert(:user) +      followed = insert(:user, %{info: %{"locked" => true}}) + +      {:ok, follow_activity} = ActivityPub.follow(follower, followed) + +      accept_data = +        File.read!("test/fixtures/mastodon-accept-activity.json") +        |> Poison.decode!() +        |> Map.put("actor", followed.ap_id) +        |> Map.put("object", follow_activity.data["id"]) + +      {:ok, activity} = Transmogrifier.handle_incoming(accept_data) +      assert activity.data["object"] == follow_activity.data["id"] + +      follower = Repo.get(User, follower.id) + +      assert User.following?(follower, followed) == true +    end + +    test "it fails for incoming accepts which cannot be correlated" do +      follower = insert(:user) +      followed = insert(:user, %{info: %{"locked" => true}}) + +      accept_data = +        File.read!("test/fixtures/mastodon-accept-activity.json") +        |> Poison.decode!() +        |> Map.put("actor", followed.ap_id) + +      accept_data = +        Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) + +      :error = Transmogrifier.handle_incoming(accept_data) + +      follower = Repo.get(User, follower.id) + +      refute User.following?(follower, followed) == true +    end + +    test "it fails for incoming rejects which cannot be correlated" do +      follower = insert(:user) +      followed = insert(:user, %{info: %{"locked" => true}}) + +      accept_data = +        File.read!("test/fixtures/mastodon-reject-activity.json") +        |> Poison.decode!() +        |> Map.put("actor", followed.ap_id) + +      accept_data = +        Map.put(accept_data, "object", Map.put(accept_data["object"], "actor", follower.ap_id)) + +      :error = Transmogrifier.handle_incoming(accept_data) + +      follower = Repo.get(User, follower.id) + +      refute User.following?(follower, followed) == true +    end + +    test "it works for incoming rejects which are orphaned" do +      follower = insert(:user) +      followed = insert(:user, %{info: %{"locked" => true}}) + +      {:ok, follower} = User.follow(follower, followed) +      {:ok, _follow_activity} = ActivityPub.follow(follower, followed) + +      assert User.following?(follower, followed) == true + +      reject_data = +        File.read!("test/fixtures/mastodon-reject-activity.json") +        |> Poison.decode!() +        |> Map.put("actor", followed.ap_id) + +      reject_data = +        Map.put(reject_data, "object", Map.put(reject_data["object"], "actor", follower.ap_id)) + +      {:ok, activity} = Transmogrifier.handle_incoming(reject_data) +      refute activity.local + +      follower = Repo.get(User, follower.id) + +      assert User.following?(follower, followed) == false +    end + +    test "it works for incoming rejects which are referenced by IRI only" do +      follower = insert(:user) +      followed = insert(:user, %{info: %{"locked" => true}}) + +      {:ok, follower} = User.follow(follower, followed) +      {:ok, follow_activity} = ActivityPub.follow(follower, followed) + +      assert User.following?(follower, followed) == true + +      reject_data = +        File.read!("test/fixtures/mastodon-reject-activity.json") +        |> Poison.decode!() +        |> Map.put("actor", followed.ap_id) +        |> Map.put("object", follow_activity.data["id"]) + +      {:ok, %Activity{data: _}} = Transmogrifier.handle_incoming(reject_data) + +      follower = Repo.get(User, follower.id) + +      assert User.following?(follower, followed) == false +    end    end    describe "prepare outgoing" do diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index 0f091b986..936d27182 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -298,7 +298,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do      test "list timeline", %{conn: conn} do        user = insert(:user)        other_user = insert(:user) -      {:ok, activity_one} = TwitterAPI.create_status(user, %{"status" => "Marisa is cute."}) +      {:ok, _activity_one} = TwitterAPI.create_status(user, %{"status" => "Marisa is cute."})        {:ok, activity_two} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."})        {:ok, list} = Pleroma.List.create("name", user)        {:ok, list} = Pleroma.List.follow(list, other_user)  | 
