Couple of fixes for groups

- Fix posts update federation and add tests
- Fix posts deletion federation and add tests

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2020-10-19 09:32:37 +02:00
parent 0c4a7e0216
commit fc1d392211
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
12 changed files with 537 additions and 239 deletions

View File

@ -15,6 +15,7 @@ defmodule Mobilizon.Federation.ActivityPub do
Config, Config,
Discussions, Discussions,
Events, Events,
Posts,
Resources, Resources,
Share, Share,
Users Users
@ -88,6 +89,7 @@ defmodule Mobilizon.Federation.ActivityPub do
{:existing, Discussions.get_discussion_by_url(url)}, {:existing, Discussions.get_discussion_by_url(url)},
{:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)}, {:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)},
{:existing, nil} <- {:existing, Resources.get_resource_by_url(url)}, {:existing, nil} <- {:existing, Resources.get_resource_by_url(url)},
{:existing, nil} <- {:existing, Posts.get_post_by_url(url)},
{:existing, nil} <- {:existing, nil} <-
{:existing, Actors.get_actor_by_url_2(url)}, {:existing, Actors.get_actor_by_url_2(url)},
{:existing, nil} <- {:existing, Actors.get_member_by_url(url)}, {:existing, nil} <- {:existing, Actors.get_member_by_url(url)},
@ -109,6 +111,9 @@ defmodule Mobilizon.Federation.ActivityPub do
{:error, "Gone"} -> {:error, "Gone"} ->
{:error, "Gone", entity} {:error, "Gone", entity}
{:error, "Not found"} ->
{:error, "Not found", entity}
end end
else else
{:ok, entity} {:ok, entity}

View File

@ -50,7 +50,7 @@ defmodule Mobilizon.Federation.ActivityPub.Federator do
def handle(:incoming_ap_doc, params) do def handle(:incoming_ap_doc, params) do
Logger.info("Handling incoming AP activity") Logger.info("Handling incoming AP activity")
Logger.debug(inspect(params)) Logger.debug(inspect(Map.drop(params, ["@context"])))
case Transmogrifier.handle_incoming(params) do case Transmogrifier.handle_incoming(params) do
{:ok, activity, _data} -> {:ok, activity, _data} ->

View File

@ -32,6 +32,10 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
Logger.warn("Resource at #{url} is 410 Gone") Logger.warn("Resource at #{url} is 410 Gone")
{:error, "Gone"} {:error, "Gone"}
{:ok, %Tesla.Env{status: 404}} ->
Logger.warn("Resource at #{url} is 404 Gone")
{:error, "Not found"}
{:ok, %Tesla.Env{} = res} -> {:ok, %Tesla.Env{} = res} ->
{:error, res} {:error, res}
end end

View File

@ -4,10 +4,11 @@ defmodule Mobilizon.Federation.ActivityPub.Preloader do
""" """
# TODO: Move me in a more appropriate place # TODO: Move me in a more appropriate place
alias Mobilizon.{Actors, Discussions, Events, Resources} alias Mobilizon.{Actors, Discussions, Events, Posts, Resources}
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Posts.Post
alias Mobilizon.Resources.Resource alias Mobilizon.Resources.Resource
alias Mobilizon.Tombstone alias Mobilizon.Tombstone
@ -23,6 +24,9 @@ defmodule Mobilizon.Federation.ActivityPub.Preloader do
def maybe_preload(%Resource{url: url}), def maybe_preload(%Resource{url: url}),
do: {:ok, Resources.get_resource_by_url_with_preloads(url)} do: {:ok, Resources.get_resource_by_url_with_preloads(url)}
def maybe_preload(%Post{url: url}),
do: {:ok, Posts.get_post_by_url_with_preloads(url)}
def maybe_preload(%Actor{url: url}), do: {:ok, Actors.get_actor_by_url!(url, true)} def maybe_preload(%Actor{url: url}), do: {:ok, Actors.get_actor_by_url!(url, true)}
def maybe_preload(%Member{} = member), do: {:ok, member} def maybe_preload(%Member{} = member), do: {:ok, member}

View File

@ -118,7 +118,9 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
defp process_collection(_, _), do: :error defp process_collection(_, _), do: :error
defp handling_element(data) when is_map(data) do # If we're handling an activity
defp handling_element(%{"type" => activity_type} = data)
when activity_type in ["Create", "Update", "Delete"] do
object = get_in(data, ["object"]) object = get_in(data, ["object"])
if object do if object do
@ -128,6 +130,26 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
Transmogrifier.handle_incoming(data) Transmogrifier.handle_incoming(data)
end end
# If we're handling directly an object
defp handling_element(data) when is_map(data) do
object = get_in(data, ["object"])
if object do
object |> Utils.get_url() |> Mobilizon.Tombstone.delete_uri_tombstone()
end
activity = %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["actor"] || data["attributedTo"],
"attributedTo" => data["attributedTo"] || data["actor"],
"object" => data
}
Transmogrifier.handle_incoming(activity)
end
defp handling_element(uri) when is_binary(uri) do defp handling_element(uri) when is_binary(uri) do
ActivityPub.fetch_object_from_url(uri) ActivityPub.fetch_object_from_url(uri)
end end

View File

@ -418,6 +418,50 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end end
end end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Article"} = object, "actor" => _actor} =
update_data
) do
with actor <- Utils.get_actor(update_data),
{:ok, %Actor{url: actor_url, suspended: false} = actor} <-
ActivityPub.get_or_fetch_actor_by_url(actor),
{:ok, %Post{} = old_post} <-
object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
object_data <- Converter.Post.as_to_model_data(object),
{:origin_check, true} <-
{:origin_check,
Utils.origin_check?(actor_url, update_data["object"]) ||
Utils.activity_actor_is_group_member?(actor, old_post)},
{:ok, %Activity{} = activity, %Post{} = new_post} <-
ActivityPub.update(old_post, object_data, false) do
{:ok, activity, new_post}
else
_e ->
:error
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => type} = object, "actor" => _actor} =
update_data
)
when type in ["ResourceCollection", "Document"] do
with actor <- Utils.get_actor(update_data),
{:ok, %Actor{url: actor_url, suspended: false}} <-
ActivityPub.get_or_fetch_actor_by_url(actor),
{:ok, %Resource{} = old_resource} <-
object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
object_data <- Converter.Resource.as_to_model_data(object),
{:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)},
{:ok, %Activity{} = activity, %Resource{} = new_resource} <-
ActivityPub.update(old_resource, object_data, false) do
{:ok, activity, new_resource}
else
_e ->
:error
end
end
def handle_incoming( def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Member"} = object, "actor" => _actor} = %{"type" => "Update", "object" => %{"type" => "Member"} = object, "actor" => _actor} =
update_data update_data
@ -505,7 +549,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
with actor_url <- Utils.get_actor(data), with actor_url <- Utils.get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url), {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
object_id <- Utils.get_url(object), object_id <- Utils.get_url(object),
{:error, "Gone", object} <- ActivityPub.fetch_object_from_url(object_id, force: true), {:ok, object} <- can_delete_group_object(object_id),
{:origin_check, true} <- {:origin_check, true} <-
{:origin_check, {:origin_check,
Utils.origin_check_from_id?(actor_url, object_id) || Utils.origin_check_from_id?(actor_url, object_id) ||
@ -975,4 +1019,25 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
fetch_object_optionnally_authenticated(url, actor) fetch_object_optionnally_authenticated(url, actor)
end end
end end
defp can_delete_group_object(object_id) do
case ActivityPub.fetch_object_from_url(object_id, force: true) do
{:error, error_message, object} when error_message in ["Gone", "Not found"] ->
{:ok, object}
{:ok, %{url: url} = object} ->
if Utils.are_same_origin?(url, Endpoint.url()),
do: {:ok, object},
else: {:error, "Group object URL remote"}
{:error, {:error, err}} ->
{:error, err}
{:error, err} ->
{:error, err}
err ->
err
end
end
end end

View File

@ -1,6 +1,6 @@
defmodule Mobilizon.Federation.ActivityPub.Types.Posts do defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
@moduledoc false @moduledoc false
alias Mobilizon.{Actors, Posts} alias Mobilizon.{Actors, Posts, Tombstone}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.Types.Entity alias Mobilizon.Federation.ActivityPub.Types.Entity
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
@ -11,6 +11,8 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
@behaviour Entity @behaviour Entity
@public_ap "https://www.w3.org/ns/activitystreams#Public"
@impl Entity @impl Entity
def create(args, additional) do def create(args, additional) do
with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1), with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1),
@ -66,7 +68,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
def delete( def delete(
%Post{ %Post{
url: url, url: url,
attributed_to: %Actor{url: group_url} attributed_to: %Actor{url: group_url, members_url: members_url}
} = post, } = post,
%Actor{url: actor_url} = actor, %Actor{url: actor_url} = actor,
_local, _local,
@ -77,11 +79,13 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
"type" => "Delete", "type" => "Delete",
"object" => Convertible.model_to_as(post), "object" => Convertible.model_to_as(post),
"id" => url <> "/delete", "id" => url <> "/delete",
"to" => [group_url] "to" => [group_url, @public_ap, members_url]
} }
with {:ok, _post} <- Posts.delete_post(post), with {:ok, %Post{} = post} <- Posts.delete_post(post),
{:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}") do {:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}"),
{:ok, %Tombstone{} = _tombstone} <-
Tombstone.create_tombstone(%{uri: post.url, actor_id: actor.id}) do
{:ok, activity_data, actor, post} {:ok, activity_data, actor, post}
end end
end end

View File

@ -287,9 +287,14 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id), def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id),
do: origin_check_from_id?(id, other_id) do: origin_check_from_id?(id, other_id)
def activity_actor_is_group_member?(%Actor{id: actor_id}, object) do def activity_actor_is_group_member?(%Actor{id: actor_id, url: actor_url}, object) do
Logger.debug(
"Checking if activity actor #{actor_url} is a member from group from #{object.url}"
)
case Ownable.group_actor(object) do case Ownable.group_actor(object) do
%Actor{type: :Group, id: group_id} -> %Actor{type: :Group, id: group_id} ->
Logger.debug("Group object ID is #{group_id}")
Actors.is_member?(actor_id, group_id) Actors.is_member?(actor_id, group_id)
_ -> _ ->

View File

@ -0,0 +1,212 @@
defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.DeleteTest do
use Mobilizon.DataCase
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
use Oban.Testing, repo: Mobilizon.Storage.Repo
import Mobilizon.Factory
import ExUnit.CaptureLog
import Mox
alias Mobilizon.{Actors, Discussions, Events, Posts}
alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event
alias Mobilizon.Posts.Post
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
alias Mobilizon.Federation.ActivityStream.Convertible
alias Mobilizon.Service.HTTP.ActivityPub.Mock
describe "handle incoming delete activities" do
test "it works for incoming deletes" do
%Actor{url: actor_url} =
actor = insert(:actor, url: "http://mobilizon.tld/@remote", domain: "mobilizon.tld")
%Comment{url: comment_url} =
insert(:comment,
actor: nil,
actor_id: actor.id,
url: "http://mobilizon.tld/comments/9f3794b8-11a0-4a98-8cb7-475ab332c701"
)
Mock
|> expect(:call, fn
%{method: :get, url: "http://mobilizon.tld/comments/9f3794b8-11a0-4a98-8cb7-475ab332c701"},
_opts ->
{:ok, %Tesla.Env{status: 410, body: "Gone"}}
end)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Jason.decode!()
object =
data["object"]
|> Map.put("id", comment_url)
data =
data
|> Map.put("object", object)
|> Map.put("actor", actor_url)
assert Discussions.get_comment_from_url(comment_url)
assert is_nil(Discussions.get_comment_from_url(comment_url).deleted_at)
{:ok, %Activity{local: false}, _} = Transmogrifier.handle_incoming(data)
refute is_nil(Discussions.get_comment_from_url(comment_url).deleted_at)
end
test "it fails for incoming deletes with spoofed origin" do
comment = insert(:comment)
announce_data =
File.read!("test/fixtures/mastodon-announce.json")
|> Jason.decode!()
|> Map.put("object", comment.url)
{:ok, _, _} = Transmogrifier.handle_incoming(announce_data)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Jason.decode!()
object =
data["object"]
|> Map.put("id", comment.url)
data =
data
|> Map.put("object", object)
:error = Transmogrifier.handle_incoming(data)
assert Discussions.get_comment_from_url(comment.url)
end
setup :set_mox_from_context
test "it works for incoming actor deletes" do
%Actor{url: url} =
actor = insert(:actor, url: "https://framapiaf.org/users/admin", domain: "framapiaf.org")
%Event{url: event1_url} = event1 = insert(:event, organizer_actor: actor)
insert(:event, organizer_actor: actor)
%Comment{url: comment1_url} = comment1 = insert(:comment, actor: actor)
insert(:comment, actor: actor)
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Jason.decode!()
Mock
|> expect(:call, fn
%{method: :get, url: "https://framapiaf.org/users/admin"}, _opts ->
{:ok, %Tesla.Env{status: 410, body: "Gone"}}
end)
{:ok, _activity, _actor} = Transmogrifier.handle_incoming(data)
assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :background)
assert {:error, :actor_not_found} = Actors.get_actor_by_url(url)
assert {:error, :event_not_found} = Events.get_event(event1.id)
# Tombstone are cascade deleted, seems correct for now
# assert %Tombstone{} = Tombstone.find_tombstone(event1_url)
assert %Comment{deleted_at: deleted_at} = Discussions.get_comment(comment1.id)
refute is_nil(deleted_at)
# assert %Tombstone{} = Tombstone.find_tombstone(comment1_url)
end
test "it fails for incoming actor deletes with spoofed origin" do
%{url: url} = insert(:actor)
deleted_actor_url = "https://framapiaf.org/users/admin"
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Jason.decode!()
|> Map.put("actor", url)
deleted_actor_data =
File.read!("test/fixtures/mastodon-actor.json")
|> Jason.decode!()
|> Map.put("id", deleted_actor_url)
Mock
|> expect(:call, fn
%{url: ^deleted_actor_url}, _opts ->
{:ok, %Tesla.Env{status: 200, body: deleted_actor_data}}
end)
assert capture_log(fn ->
assert :error == Transmogrifier.handle_incoming(data)
end) =~ "Object origin check failed"
assert Actors.get_actor_by_url(url)
end
end
describe "handle incoming delete activities for group posts" do
test "works for remote deletions" do
%Actor{url: remote_actor_url} =
insert(:actor,
domain: "remote.domain",
url: "https://remote.domain/@remote",
preferred_username: "remote"
)
group = insert(:group)
%Post{} = post = insert(:post, attributed_to: group)
data = Convertible.model_to_as(post)
refute is_nil(Posts.get_post_by_url(data["id"]))
delete_data =
File.read!("test/fixtures/mastodon-delete.json")
|> Jason.decode!()
object =
data
|> Map.put("type", "Article")
delete_data =
delete_data
|> Map.put("actor", remote_actor_url)
|> Map.put("object", object)
:error = Transmogrifier.handle_incoming(delete_data)
refute is_nil(Posts.get_post_by_url(data["id"]))
end
test "doesn't work for remote deletions if the actor is not a group member" do
%Actor{url: remote_actor_url} =
insert(:actor,
domain: "remote.domain",
url: "https://remote.domain/@remote",
preferred_username: "remote"
)
group = insert(:group)
%Post{} = post = insert(:post, attributed_to: group)
data = Convertible.model_to_as(post)
refute is_nil(Posts.get_post_by_url(data["id"]))
delete_data =
File.read!("test/fixtures/mastodon-delete.json")
|> Jason.decode!()
object =
data
|> Map.put("type", "Article")
delete_data =
delete_data
|> Map.put("actor", remote_actor_url)
|> Map.put("object", object)
:error = Transmogrifier.handle_incoming(delete_data)
refute is_nil(Posts.get_post_by_url(data["id"]))
end
end
end

View File

@ -0,0 +1,181 @@
defmodule Mobilizon.Federation.ActivityPub.Transmogrifier.UpdateTest do
use Mobilizon.DataCase
use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney
use Oban.Testing, repo: Mobilizon.Storage.Repo
import Mobilizon.Factory
alias Mobilizon.{Actors, Events, Posts}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Posts.Post
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
alias Mobilizon.Federation.ActivityStream.Convertible
describe "handle incoming update activities" do
test "it works for incoming update activities on actors" do
use_cassette "activity_pub/update_actor_activity" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
update_data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("id", data["actor"])
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: _data, local: false}, _} =
Transmogrifier.handle_incoming(update_data)
{:ok, %Actor{} = actor} = Actors.get_actor_by_url(update_data["actor"])
assert actor.name == "nextsoft"
assert actor.summary == "<p>Some bio</p>"
end
end
test "it works for incoming update activities on events" do
use_cassette "activity_pub/event_update_activities" do
data = File.read!("test/fixtures/mobilizon-post-activity.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}, %Event{id: event_id}} =
Transmogrifier.handle_incoming(data)
assert_enqueued(
worker: Mobilizon.Service.Workers.BuildSearch,
args: %{event_id: event_id, op: :insert_search_event}
)
assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :search)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("name", "My updated event")
|> Map.put("id", data["object"]["id"])
|> Map.put("type", "Event")
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}, _} =
Transmogrifier.handle_incoming(update_data)
%Event{} = event = Events.get_event_by_url(data["object"]["id"])
assert_enqueued(
worker: Mobilizon.Service.Workers.BuildSearch,
args: %{event_id: event_id, op: :update_search_event}
)
assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :search)
assert event.title == "My updated event"
assert event.description == data["object"]["content"]
end
end
test "it works for incoming update activities on group posts" do
use_cassette "activity_pub/group_post_update_activities" do
%Actor{url: remote_actor_url} = remote_actor = insert(:actor, domain: "remote.domain")
group = insert(:group)
insert(:member, actor: remote_actor, parent: group)
%Post{} = post = insert(:post, attributed_to: group)
data = Convertible.model_to_as(post)
refute is_nil(Posts.get_post_by_url(data["id"]))
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
data
|> Map.put("actor", remote_actor_url)
|> Map.put("name", "My updated post")
|> Map.put("type", "Article")
update_data =
update_data
|> Map.put("actor", remote_actor_url)
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}, _} =
Transmogrifier.handle_incoming(update_data)
%Post{id: updated_post_id, title: updated_post_title} =
Posts.get_post_by_url(data["object"]["id"])
assert updated_post_id == post.id
assert updated_post_title == "My updated post"
end
end
test "it fails for incoming update activities on group posts when the actor is not a member from the group" do
use_cassette "activity_pub/group_post_update_activities" do
%Actor{url: remote_actor_url} =
insert(:actor,
domain: "remote.domain",
url: "https://remote.domain/@remote",
preferred_username: "remote"
)
group = insert(:group)
%Post{} = post = insert(:post, attributed_to: group)
data = Convertible.model_to_as(post)
refute is_nil(Posts.get_post_by_url(data["id"]))
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
data
|> Map.put("name", "My updated post")
|> Map.put("type", "Article")
update_data =
update_data
|> Map.put("actor", remote_actor_url)
|> Map.put("object", object)
:error = Transmogrifier.handle_incoming(update_data)
%Post{id: updated_post_id, title: updated_post_title} = Posts.get_post_by_url(data["id"])
assert updated_post_id == post.id
refute updated_post_title == "My updated post"
end
end
# test "it works for incoming update activities which lock the account" do
# data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
# {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
# update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
# object =
# update_data["object"]
# |> Map.put("actor", data["actor"])
# |> Map.put("id", data["actor"])
# |> Map.put("manuallyApprovesFollowers", true)
# update_data =
# update_data
# |> Map.put("actor", data["actor"])
# |> Map.put("object", object)
# {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
# user = User.get_cached_by_ap_id(data["actor"])
# assert user.info["locked"] == true
# end
end
end

View File

@ -10,11 +10,10 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
use Oban.Testing, repo: Mobilizon.Storage.Repo use Oban.Testing, repo: Mobilizon.Storage.Repo
import Mobilizon.Factory import Mobilizon.Factory
import ExUnit.CaptureLog
import Mock import Mock
import Mox import Mox
alias Mobilizon.{Actors, Discussions, Events} alias Mobilizon.{Actors, Discussions}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
@ -707,233 +706,6 @@ defmodule Mobilizon.Federation.ActivityPub.TransmogrifierTest do
end end
end end
describe "handle incoming update activities" do
test "it works for incoming update activities on actors" do
use_cassette "activity_pub/update_actor_activity" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
update_data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("id", data["actor"])
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: _data, local: false}, _} =
Transmogrifier.handle_incoming(update_data)
{:ok, %Actor{} = actor} = Actors.get_actor_by_url(update_data["actor"])
assert actor.name == "nextsoft"
assert actor.summary == "<p>Some bio</p>"
end
end
test "it works for incoming update activities on events" do
use_cassette "activity_pub/event_update_activities" do
data = File.read!("test/fixtures/mobilizon-post-activity.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}, %Event{id: event_id}} =
Transmogrifier.handle_incoming(data)
assert_enqueued(
worker: Mobilizon.Service.Workers.BuildSearch,
args: %{event_id: event_id, op: :insert_search_event}
)
assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :search)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
object =
data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("name", "My updated event")
|> Map.put("id", data["object"]["id"])
|> Map.put("type", "Event")
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}, _} =
Transmogrifier.handle_incoming(update_data)
%Event{} = event = Events.get_event_by_url(data["object"]["id"])
assert_enqueued(
worker: Mobilizon.Service.Workers.BuildSearch,
args: %{event_id: event_id, op: :update_search_event}
)
assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :search)
assert event.title == "My updated event"
assert event.description == data["object"]["content"]
end
end
# test "it works for incoming update activities which lock the account" do
# data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
# {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
# update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
# object =
# update_data["object"]
# |> Map.put("actor", data["actor"])
# |> Map.put("id", data["actor"])
# |> Map.put("manuallyApprovesFollowers", true)
# update_data =
# update_data
# |> Map.put("actor", data["actor"])
# |> Map.put("object", object)
# {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
# user = User.get_cached_by_ap_id(data["actor"])
# assert user.info["locked"] == true
# end
end
describe "handle incoming delete activities" do
test "it works for incoming deletes" do
%Actor{url: actor_url} =
actor = insert(:actor, url: "http://mobilizon.tld/@remote", domain: "mobilizon.tld")
%Comment{url: comment_url} =
insert(:comment,
actor: nil,
actor_id: actor.id,
url: "http://mobilizon.tld/comments/9f3794b8-11a0-4a98-8cb7-475ab332c701"
)
Mock
|> expect(:call, fn
%{method: :get, url: "http://mobilizon.tld/comments/9f3794b8-11a0-4a98-8cb7-475ab332c701"},
_opts ->
{:ok, %Tesla.Env{status: 410, body: "Gone"}}
end)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Jason.decode!()
object =
data["object"]
|> Map.put("id", comment_url)
data =
data
|> Map.put("object", object)
|> Map.put("actor", actor_url)
assert Discussions.get_comment_from_url(comment_url)
assert is_nil(Discussions.get_comment_from_url(comment_url).deleted_at)
{:ok, %Activity{local: false}, _} = Transmogrifier.handle_incoming(data)
refute is_nil(Discussions.get_comment_from_url(comment_url).deleted_at)
end
test "it fails for incoming deletes with spoofed origin" do
comment = insert(:comment)
announce_data =
File.read!("test/fixtures/mastodon-announce.json")
|> Jason.decode!()
|> Map.put("object", comment.url)
{:ok, _, _} = Transmogrifier.handle_incoming(announce_data)
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Jason.decode!()
object =
data["object"]
|> Map.put("id", comment.url)
data =
data
|> Map.put("object", object)
:error = Transmogrifier.handle_incoming(data)
assert Discussions.get_comment_from_url(comment.url)
end
setup :set_mox_from_context
test "it works for incoming actor deletes" do
%Actor{url: url} =
actor = insert(:actor, url: "https://framapiaf.org/users/admin", domain: "framapiaf.org")
%Event{url: event1_url} = event1 = insert(:event, organizer_actor: actor)
insert(:event, organizer_actor: actor)
%Comment{url: comment1_url} = comment1 = insert(:comment, actor: actor)
insert(:comment, actor: actor)
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Jason.decode!()
Mock
|> expect(:call, fn
%{method: :get, url: "https://framapiaf.org/users/admin"}, _opts ->
{:ok, %Tesla.Env{status: 410, body: "Gone"}}
end)
{:ok, _activity, _actor} = Transmogrifier.handle_incoming(data)
assert %{success: 1, failure: 0} == Oban.drain_queue(queue: :background)
assert {:error, :actor_not_found} = Actors.get_actor_by_url(url)
assert {:error, :event_not_found} = Events.get_event(event1.id)
# Tombstone are cascade deleted, seems correct for now
# assert %Tombstone{} = Tombstone.find_tombstone(event1_url)
assert %Comment{deleted_at: deleted_at} = Discussions.get_comment(comment1.id)
refute is_nil(deleted_at)
# assert %Tombstone{} = Tombstone.find_tombstone(comment1_url)
end
test "it fails for incoming actor deletes with spoofed origin" do
%{url: url} = insert(:actor)
deleted_actor_url = "https://framapiaf.org/users/admin"
data =
File.read!("test/fixtures/mastodon-delete-user.json")
|> Jason.decode!()
|> Map.put("actor", url)
deleted_actor_data =
File.read!("test/fixtures/mastodon-actor.json")
|> Jason.decode!()
|> Map.put("id", deleted_actor_url)
Mock
|> expect(:call, fn
%{url: ^deleted_actor_url}, _opts ->
{:ok, %Tesla.Env{status: 200, body: deleted_actor_data}}
end)
assert capture_log(fn ->
assert :error == Transmogrifier.handle_incoming(data)
end) =~ "Object origin check failed"
assert Actors.get_actor_by_url(url)
end
end
describe "handle tombstones" do describe "handle tombstones" do
setup :verify_on_exit! setup :verify_on_exit!

View File

@ -0,0 +1,24 @@
[
{
"request": {
"body": "{\"@context\":[\"https://www.w3.org/ns/activitystreams\",\"https://litepub.social/litepub/context.jsonld\",{\"Hashtag\":\"as:Hashtag\",\"PostalAddress\":\"sc:PostalAddress\",\"address\":{\"@id\":\"sc:address\",\"@type\":\"sc:PostalAddress\"},\"addressCountry\":\"sc:addressCountry\",\"addressLocality\":\"sc:addressLocality\",\"addressRegion\":\"sc:addressRegion\",\"anonymousParticipationEnabled\":{\"@id\":\"mz:anonymousParticipationEnabled\",\"@type\":\"sc:Boolean\"},\"category\":\"sc:category\",\"commentsEnabled\":{\"@id\":\"pt:commentsEnabled\",\"@type\":\"sc:Boolean\"},\"discoverable\":\"toot:discoverable\",\"ical\":\"http://www.w3.org/2002/12/cal/ical#\",\"joinMode\":{\"@id\":\"mz:joinMode\",\"@type\":\"mz:joinModeType\"},\"joinModeType\":{\"@id\":\"mz:joinModeType\",\"@type\":\"rdfs:Class\"},\"location\":{\"@id\":\"sc:location\",\"@type\":\"sc:Place\"},\"manuallyApprovesFollowers\":\"as:manuallyApprovesFollowers\",\"maximumAttendeeCapacity\":\"sc:maximumAttendeeCapacity\",\"mz\":\"https://joinmobilizon.org/ns#\",\"participationMessage\":{\"@id\":\"mz:participationMessage\",\"@type\":\"sc:Text\"},\"postalCode\":\"sc:postalCode\",\"pt\":\"https://joinpeertube.org/ns#\",\"repliesModerationOption\":{\"@id\":\"mz:repliesModerationOption\",\"@type\":\"mz:repliesModerationOptionType\"},\"repliesModerationOptionType\":{\"@id\":\"mz:repliesModerationOptionType\",\"@type\":\"rdfs:Class\"},\"sc\":\"http://schema.org#\",\"streetAddress\":\"sc:streetAddress\",\"toot\":\"http://joinmastodon.org/ns#\",\"uuid\":\"sc:identifier\"}],\"actor\":\"http://mobilizon.test/@myGroup0\",\"cc\":[],\"id\":\"http://mobilizon.test/announces/839e0ffc-f437-48db-afba-9ce1e971e938\",\"object\":{\"actor\":\"http://mobilizon.test/@thomas0\",\"attributedTo\":\"http://mobilizon.test/@myGroup0\",\"content\":\"The <b>HTML</b>body for my Article\",\"id\":\"http://mobilizon.test/p/6a482d5f-94fc-446b-84bb-d4d386d5dd45\",\"name\":\"My updated post\",\"published\":\"2020-10-19T08:37:52Z\",\"type\":\"Article\"},\"to\":[\"http://mobilizon.test/@myGroup0/members\"],\"type\":\"Announce\"}",
"headers": {
"Content-Type": "application/activity+json",
"signature": "keyId=\"http://mobilizon.test/@myGroup0#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) content-length date digest host\",signature=\"P+7rSSUeUBdX74wbvSEe4roG7yh7MfpF6s4tjv5q1kbeVKtXZRyfC1LqgVNCADZYXFqYlMvfF7DiaRQRiMznGWawM/QXK08eXiAVihYK28Pa56BfI68OUakd+FptlwfB4WJ4Jc7xi1z+iarv+EvlFxjkG5pgwL4mW49rvNnigELzypGtp2bj/2BhiBItHutvOju1MwLR1EBQFJBSZDVZZKbHTcV4KbGtbYvkWUbH8fZbe3fgctKlvO/z9kw+yBTTIEE1O18F4HiJ17nYtaaxv3/vl5RxcjYLpf+QQzkaPOsSLZs8zpIZZp3BbLtPh+OGwkyK9PBQsaI0N1ZSLQ5gaQ==\"",
"digest": "SHA-256=EyZ+uZ/Vv2lUK8ozgOHBpnoUWUM5WQHATQb1tEMldNU=",
"date": "Mon, 19 Oct 2020 08:37:52 GMT"
},
"method": "post",
"options": [],
"request_body": "",
"url": "http://mobilizon.test/inbox"
},
"response": {
"binary": false,
"body": "nxdomain",
"headers": [],
"status_code": null,
"type": "error"
}
}
]