Merge branch 'kaniini/pleroma-feature/activitypub-accept-reject-conformance' into develop

This commit is contained in:
lain 2018-05-26 15:15:52 +02:00
commit 745072b2cc
13 changed files with 327 additions and 30 deletions

View file

@ -13,7 +13,7 @@ def call(%{assigns: %{valid_signature: true}} = conn, _opts) 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")

View file

@ -67,7 +67,8 @@ def user_info(%User{} = 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 @@ def register_changeset(struct, params \\ %{}) 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

View file

@ -95,6 +95,17 @@ def accept(%{to: to, actor: actor, object: object} = params) 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 @@ def user_data_from_user_object(data) do
"url" => [%{"href" => data["image"]["url"]}]
}
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
user_data = %{
@ -471,7 +483,8 @@ def user_data_from_user_object(data) do
info: %{
"ap_enabled" => true,
"source_data" => data,
"banner" => banner
"banner" => banner,
"locked" => locked
},
avatar: avatar,
nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}",

View file

@ -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 @@ def handle_incoming(
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 @@ def handle_incoming(
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 @@ def handle_incoming(
end
end
# TODO
# Accept
def handle_incoming(_), do: :error
def get_obj_helper(id) do

View file

@ -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

View file

@ -26,7 +26,7 @@ def render("user.json", %{user: user}) 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,

View file

@ -32,14 +32,14 @@ def validate(headers, signature, public_key) 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)

View file

@ -429,7 +429,7 @@ def following(conn, %{"id" => id}) 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 @@ def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) 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

View file

@ -19,7 +19,7 @@ def render("account.json", %{user: user}) 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,

View file

@ -25,7 +25,7 @@ def delete(%User{} = user, id) 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

View file

@ -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"
}
]
}

View file

@ -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 @@ test "it works for incoming deletes" 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 @@ test "it works for incoming unblocks with an existing block" 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

View file

@ -298,7 +298,7 @@ test "deleting a list", %{conn: conn} 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)