Merge branch 'issue/1177' into 'develop'

[#1177] fixed unfollow for relay actor

See merge request pleroma/pleroma!1589
This commit is contained in:
kaniini 2019-08-25 19:43:27 +00:00
commit 6dc24422dc
10 changed files with 144 additions and 41 deletions

View File

@ -117,9 +117,7 @@ defp maybe_date_fetch(headers, date) do
def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do
Logger.info("Fetching object #{id} via AP") Logger.info("Fetching object #{id} via AP")
date = date = Pleroma.Signature.signed_date()
NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
headers = headers =
[{:Accept, "application/activity+json"}] [{:Accept, "application/activity+json"}]

View File

@ -53,4 +53,10 @@ def sign(%User{} = user, headers) do
HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers) HTTPSignatures.sign(private_key, user.ap_id <> "#main-key", headers)
end end
end end
def signed_date, do: signed_date(NaiveDateTime.utc_now())
def signed_date(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end
end end

View File

@ -41,7 +41,7 @@ def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> json(UserView.render("user.json", %{user: user}))
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
@ -53,7 +53,7 @@ def object(conn, %{"uuid" => uuid}) do
%Object{} = object <- Object.get_cached_by_ap_id(ap_id), %Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)} do {_, true} <- {:public?, Visibility.is_public?(object)} do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("object.json", %{object: object})) |> json(ObjectView.render("object.json", %{object: object}))
else else
{:public?, false} -> {:public?, false} ->
@ -69,7 +69,7 @@ def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes, page)) |> json(ObjectView.render("likes.json", ap_id, likes, page))
else else
{:public?, false} -> {:public?, false} ->
@ -83,7 +83,7 @@ def object_likes(conn, %{"uuid" => uuid}) do
{_, true} <- {:public?, Visibility.is_public?(object)}, {_, true} <- {:public?, Visibility.is_public?(object)},
likes <- Utils.get_object_likes(object) do likes <- Utils.get_object_likes(object) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes)) |> json(ObjectView.render("likes.json", ap_id, likes))
else else
{:public?, false} -> {:public?, false} ->
@ -96,7 +96,7 @@ def activity(conn, %{"uuid" => uuid}) do
%Activity{} = activity <- Activity.normalize(ap_id), %Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, Visibility.is_public?(activity)} do {_, true} <- {:public?, Visibility.is_public?(activity)} do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("object.json", %{object: activity})) |> json(ObjectView.render("object.json", %{object: activity}))
else else
{:public?, false} -> {:public?, false} ->
@ -104,6 +104,13 @@ def activity(conn, %{"uuid" => uuid}) do
end end
end end
# GET /relay/following
def following(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: Relay.get_actor()}))
end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
@ -112,12 +119,12 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, page: page, for: for_user})) |> json(UserView.render("following.json", %{user: user, page: page, for: for_user}))
else else
{:show_follows, _} -> {:show_follows, _} ->
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> send_resp(403, "") |> send_resp(403, "")
end end
end end
@ -126,11 +133,18 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, for: for_user})) |> json(UserView.render("following.json", %{user: user, for: for_user}))
end end
end end
# GET /relay/followers
def followers(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: Relay.get_actor()}))
end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user),
@ -139,12 +153,12 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, page: page, for: for_user})) |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user}))
else else
{:show_followers, _} -> {:show_followers, _} ->
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> send_resp(403, "") |> send_resp(403, "")
end end
end end
@ -153,7 +167,7 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, for: for_user})) |> json(UserView.render("followers.json", %{user: user, for: for_user}))
end end
end end
@ -162,7 +176,7 @@ def outbox(conn, %{"nickname" => nickname} = params) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]})) |> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
end end
end end
@ -210,7 +224,7 @@ def inbox(conn, params) do
defp represent_service_actor(%User{} = user, conn) do defp represent_service_actor(%User{} = user, conn) do
with {:ok, user} <- User.ensure_keys_present(user) do with {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> json(UserView.render("user.json", %{user: user}))
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
@ -231,7 +245,7 @@ def internal_fetch(conn, _params) do
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> json(UserView.render("user.json", %{user: user}))
end end
@ -240,7 +254,7 @@ def whoami(_conn, _params), do: {:error, :not_found}
def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
if nickname == user.nickname do if nickname == user.nickname do
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]})) |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))
else else
err = err =

View File

@ -50,9 +50,7 @@ def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = pa
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date = date = Pleroma.Signature.signed_date()
NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
signature = signature =
Pleroma.Signature.sign(actor, %{ Pleroma.Signature.sign(actor, %{

View File

@ -22,13 +22,7 @@ def follow(target_instance) do
Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}") Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity} {:ok, activity}
else else
{:error, _} = error -> error -> format_error(error)
Logger.error("error: #{inspect(error)}")
error
e ->
Logger.error("error: #{inspect(e)}")
{:error, e}
end end
end end
@ -37,16 +31,11 @@ def unfollow(target_instance) do
with %User{} = local_user <- get_actor(), with %User{} = local_user <- get_actor(),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance),
{:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
User.unfollow(local_user, target_user)
Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity} {:ok, activity}
else else
{:error, _} = error -> error -> format_error(error)
Logger.error("error: #{inspect(error)}")
error
e ->
Logger.error("error: #{inspect(e)}")
{:error, e}
end end
end end
@ -56,11 +45,16 @@ def publish(%Activity{data: %{"type" => "Create"}} = activity) do
%Object{} = object <- Object.normalize(activity) do %Object{} = object <- Object.normalize(activity) do
ActivityPub.announce(user, object, nil, true, false) ActivityPub.announce(user, object, nil, true, false)
else else
e -> error -> format_error(error)
Logger.error("error: #{inspect(e)}")
{:error, inspect(e)}
end end
end end
def publish(_), do: {:error, "Not implemented"} def publish(_), do: {:error, "Not implemented"}
defp format_error({:error, error}), do: format_error(error)
defp format_error(error) do
Logger.error("error: #{inspect(error)}")
{:error, error}
end
end end

View File

@ -133,6 +133,10 @@ defmodule Pleroma.Web.Router do
}) })
end end
pipeline :http_signature do
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_api) pipe_through(:pleroma_api)
@ -688,9 +692,16 @@ defmodule Pleroma.Web.Router do
pipe_through(:ap_service_actor) pipe_through(:ap_service_actor)
get("/", ActivityPubController, :relay) get("/", ActivityPubController, :relay)
scope [] do
pipe_through(:http_signature)
post("/inbox", ActivityPubController, :inbox) post("/inbox", ActivityPubController, :inbox)
end end
get("/following", ActivityPubController, :following, assigns: %{relay: true})
get("/followers", ActivityPubController, :followers, assigns: %{relay: true})
end
scope "/internal/fetch", Pleroma.Web.ActivityPub do scope "/internal/fetch", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor) pipe_through(:ap_service_actor)

View File

@ -8,6 +8,7 @@ defmodule Pleroma.SignatureTest do
import ExUnit.CaptureLog import ExUnit.CaptureLog
import Pleroma.Factory import Pleroma.Factory
import Tesla.Mock import Tesla.Mock
import Mock
alias Pleroma.Signature alias Pleroma.Signature
@ -114,4 +115,17 @@ test "it properly deduces the actor id for mastodon and pleroma" do
"https://example.com/users/1234" "https://example.com/users/1234"
end end
end end
describe "signed_date" do
test "it returns formatted current date" do
with_mock(NaiveDateTime, utc_now: fn -> ~N[2019-08-23 18:11:24.822233] end) do
assert Signature.signed_date() == "Fri, 23 Aug 2019 18:11:24 GMT"
end
end
test "it returns formatted date" do
assert Signature.signed_date(~N[2019-08-23 08:11:24.822233]) ==
"Fri, 23 Aug 2019 08:11:24 GMT"
end
end
end end

View File

@ -50,7 +50,8 @@ test "relay is unfollowed" do
%User{ap_id: follower_id} = local_user = Relay.get_actor() %User{ap_id: follower_id} = local_user = Relay.get_actor()
target_user = User.get_cached_by_ap_id(target_instance) target_user = User.get_cached_by_ap_id(target_instance)
follow_activity = Utils.fetch_latest_follow(local_user, target_user) follow_activity = Utils.fetch_latest_follow(local_user, target_user)
User.follow(local_user, target_user)
assert "#{target_instance}/followers" in refresh_record(local_user).following
Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance]) Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance])
cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"]) cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"])
@ -67,6 +68,7 @@ test "relay is unfollowed" do
assert undo_activity.data["type"] == "Undo" assert undo_activity.data["type"] == "Undo"
assert undo_activity.data["actor"] == local_user.ap_id assert undo_activity.data["actor"] == local_user.ap_id
assert undo_activity.data["object"] == cancelled_activity.data assert undo_activity.data["object"] == cancelled_activity.data
refute "#{target_instance}/followers" in refresh_record(local_user).following
end end
end end

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -593,6 +594,34 @@ test "it increases like count when receiving a like action", %{conn: conn} do
end end
end end
describe "/relay/followers" do
test "it returns relay followers", %{conn: conn} do
relay_actor = Relay.get_actor()
user = insert(:user)
User.follow(user, relay_actor)
result =
conn
|> assign(:relay, true)
|> get("/relay/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
end
describe "/relay/following" do
test "it returns relay following", %{conn: conn} do
result =
conn
|> assign(:relay, true)
|> get("/relay/following")
|> json_response(200)
assert result["first"]["orderedItems"] == []
end
end
describe "/users/:nickname/followers" do describe "/users/:nickname/followers" do
test "it returns the followers in a collection", %{conn: conn} do test "it returns the followers in a collection", %{conn: conn} do
user = insert(:user) user = insert(:user)

View File

@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.RelayTest do
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
import Pleroma.Factory import Pleroma.Factory
import Mock
test "gets an actor for the relay" do test "gets an actor for the relay" do
user = Relay.get_actor() user = Relay.get_actor()
@ -43,16 +44,21 @@ test "returns activity" do
user = insert(:user) user = insert(:user)
service_actor = Relay.get_actor() service_actor = Relay.get_actor()
ActivityPub.follow(service_actor, user) ActivityPub.follow(service_actor, user)
Pleroma.User.follow(service_actor, user)
assert "#{user.ap_id}/followers" in refresh_record(service_actor).following
assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id) assert {:ok, %Activity{} = activity} = Relay.unfollow(user.ap_id)
assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay" assert activity.actor == "#{Pleroma.Web.Endpoint.url()}/relay"
assert user.ap_id in activity.recipients assert user.ap_id in activity.recipients
assert activity.data["type"] == "Undo" assert activity.data["type"] == "Undo"
assert activity.data["actor"] == service_actor.ap_id assert activity.data["actor"] == service_actor.ap_id
assert activity.data["to"] == [user.ap_id] assert activity.data["to"] == [user.ap_id]
refute "#{user.ap_id}/followers" in refresh_record(service_actor).following
end end
end end
describe "publish/1" do describe "publish/1" do
clear_config([:instance, :federating])
test "returns error when activity not `Create` type" do test "returns error when activity not `Create` type" do
activity = insert(:like_activity) activity = insert(:like_activity)
assert Relay.publish(activity) == {:error, "Not implemented"} assert Relay.publish(activity) == {:error, "Not implemented"}
@ -63,13 +69,44 @@ test "returns error when activity not public" do
assert Relay.publish(activity) == {:error, false} assert Relay.publish(activity) == {:error, false}
end end
test "returns announce activity" do test "returns error when object is unknown" do
activity =
insert(:note_activity,
data: %{
"type" => "Create",
"object" => "http://mastodon.example.org/eee/99541947525187367"
}
)
assert Relay.publish(activity) == {:error, nil}
end
test_with_mock "returns announce activity and publish to federate",
Pleroma.Web.Federator,
[:passthrough],
[] do
Pleroma.Config.put([:instance, :federating], true)
service_actor = Relay.get_actor() service_actor = Relay.get_actor()
note = insert(:note_activity) note = insert(:note_activity)
assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note) assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note)
assert activity.data["type"] == "Announce" assert activity.data["type"] == "Announce"
assert activity.data["actor"] == service_actor.ap_id assert activity.data["actor"] == service_actor.ap_id
assert activity.data["object"] == obj.data["id"] assert activity.data["object"] == obj.data["id"]
assert called(Pleroma.Web.Federator.publish(activity, 5))
end
test_with_mock "returns announce activity and not publish to federate",
Pleroma.Web.Federator,
[:passthrough],
[] do
Pleroma.Config.put([:instance, :federating], false)
service_actor = Relay.get_actor()
note = insert(:note_activity)
assert {:ok, %Activity{} = activity, %Object{} = obj} = Relay.publish(note)
assert activity.data["type"] == "Announce"
assert activity.data["actor"] == service_actor.ap_id
assert activity.data["object"] == obj.data["id"]
refute called(Pleroma.Web.Federator.publish(activity, 5))
end end
end end
end end