@@ -14,7 +14,15 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Create do
|
||||
]
|
||||
|
||||
@type create_entities ::
|
||||
:event | :comment | :discussion | :actor | :todo_list | :todo | :resource | :post
|
||||
:event
|
||||
| :comment
|
||||
| :discussion
|
||||
| :conversation
|
||||
| :actor
|
||||
| :todo_list
|
||||
| :todo
|
||||
| :resource
|
||||
| :post
|
||||
|
||||
@doc """
|
||||
Create an activity of type `Create`
|
||||
@@ -50,18 +58,27 @@ defmodule Mobilizon.Federation.ActivityPub.Actions.Create do
|
||||
end
|
||||
end
|
||||
|
||||
@map_types %{
|
||||
:event => Types.Events,
|
||||
:comment => Types.Comments,
|
||||
:discussion => Types.Discussions,
|
||||
:conversation => Types.Conversations,
|
||||
:actor => Types.Actors,
|
||||
:todo_list => Types.TodoLists,
|
||||
:todo => Types.Todos,
|
||||
:resource => Types.Resources,
|
||||
:post => Types.Posts
|
||||
}
|
||||
|
||||
@spec do_create(create_entities(), map(), map()) ::
|
||||
{:ok, Entity.t(), Activity.t()} | {:error, Ecto.Changeset.t() | atom()}
|
||||
defp do_create(type, args, additional) do
|
||||
case type do
|
||||
:event -> Types.Events.create(args, additional)
|
||||
:comment -> Types.Comments.create(args, additional)
|
||||
:discussion -> Types.Discussions.create(args, additional)
|
||||
:actor -> Types.Actors.create(args, additional)
|
||||
:todo_list -> Types.TodoLists.create(args, additional)
|
||||
:todo -> Types.Todos.create(args, additional)
|
||||
:resource -> Types.Resources.create(args, additional)
|
||||
:post -> Types.Posts.create(args, additional)
|
||||
mod = Map.get(@map_types, type)
|
||||
|
||||
if is_nil(mod) do
|
||||
{:error, :type_not_supported}
|
||||
else
|
||||
mod.create(args, additional)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||
|
||||
alias Mobilizon.{Actors, Discussions, Events, Share}
|
||||
alias Mobilizon.Actors.{Actor, Member}
|
||||
alias Mobilizon.Conversations.Conversation
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
@@ -38,6 +39,10 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||
%{"to" => maybe_add_group_members([], actor), "cc" => []}
|
||||
end
|
||||
|
||||
def get_audience(%Conversation{participants: participants}) do
|
||||
%{"to" => Enum.map(participants, & &1.url), "cc" => []}
|
||||
end
|
||||
|
||||
# Deleted comments are just like tombstones
|
||||
def get_audience(%Comment{deleted_at: deleted_at}) when not is_nil(deleted_at) do
|
||||
%{"to" => [@ap_public], "cc" => []}
|
||||
|
||||
@@ -177,7 +177,7 @@ defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
{:error, :content_not_json}
|
||||
|
||||
{:ok, %Tesla.Env{} = res} ->
|
||||
Logger.debug("Resource returned bad HTTP code #{inspect(res)}")
|
||||
Logger.debug("Resource returned bad HTTP code (#{res.status}) #{inspect(res)}")
|
||||
{:error, :http_error}
|
||||
|
||||
{:error, err} ->
|
||||
|
||||
@@ -68,24 +68,26 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object}) do
|
||||
Logger.info("Handle incoming to create notes")
|
||||
|
||||
case Converter.Comment.as_to_model_data(object) do
|
||||
%{visibility: visibility, event_id: event_id}
|
||||
when visibility != :public and event_id != nil ->
|
||||
Logger.info("Tried to reply to an event with a private comment - ignore")
|
||||
:error
|
||||
case Discussions.get_comment_from_url_with_preload(object["id"]) do
|
||||
{:error, :comment_not_found} ->
|
||||
case Converter.Comment.as_to_model_data(object) do
|
||||
%{visibility: visibility} = object_data
|
||||
when visibility === :private ->
|
||||
Actions.Create.create(:conversation, object_data, false)
|
||||
|
||||
object_data when is_map(object_data) ->
|
||||
case Discussions.get_comment_from_url_with_preload(object_data.url) do
|
||||
{:error, :comment_not_found} ->
|
||||
object_data
|
||||
|> transform_object_data_for_discussion()
|
||||
|> save_comment_or_discussion()
|
||||
|
||||
{:ok, %Comment{} = comment} ->
|
||||
# Object already exists
|
||||
{:ok, nil, comment}
|
||||
object_data when is_map(object_data) ->
|
||||
case Discussions.get_comment_from_url_with_preload(object_data.url) do
|
||||
{:error, :comment_not_found} ->
|
||||
object_data
|
||||
|> transform_object_data_for_discussion()
|
||||
|> save_comment_or_discussion()
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, %Comment{} = comment} ->
|
||||
# Object already exists
|
||||
{:ok, nil, comment}
|
||||
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
207
lib/federation/activity_pub/types/conversation.ex
Normal file
207
lib/federation/activity_pub/types/conversation.ex
Normal file
@@ -0,0 +1,207 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Conversations do
|
||||
@moduledoc false
|
||||
|
||||
# alias Mobilizon.Conversations.ConversationParticipant
|
||||
alias Mobilizon.{Actors, Conversations, Discussions}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Conversation
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||
alias Mobilizon.Federation.ActivityPub.{Audience, Permission}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
alias Mobilizon.Service.Activity.Conversation, as: ConversationActivity
|
||||
alias Mobilizon.Web.Endpoint
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
|
||||
require Logger
|
||||
|
||||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) ::
|
||||
{:ok, Conversation.t(), ActivityStream.t()}
|
||||
| {:error, :conversation_not_found | :last_comment_not_found | Ecto.Changeset.t()}
|
||||
def create(%{conversation_id: conversation_id} = args, additional)
|
||||
when not is_nil(conversation_id) do
|
||||
Logger.debug("Creating a reply to a conversation #{inspect(args, pretty: true)}")
|
||||
args = prepare_args(args)
|
||||
Logger.debug("Creating a reply to a conversation #{inspect(args, pretty: true)}")
|
||||
|
||||
case Conversations.get_conversation(conversation_id) do
|
||||
%Conversation{} = conversation ->
|
||||
case Conversations.reply_to_conversation(conversation, args) do
|
||||
{:ok, %Conversation{last_comment_id: last_comment_id} = conversation} ->
|
||||
ConversationActivity.insert_activity(conversation, subject: "conversation_replied")
|
||||
maybe_publish_graphql_subscription(conversation)
|
||||
|
||||
case Discussions.get_comment_with_preload(last_comment_id) do
|
||||
%Comment{} = last_comment ->
|
||||
comment_as_data = Convertible.model_to_as(last_comment)
|
||||
audience = Audience.get_audience(conversation)
|
||||
create_data = make_create_data(comment_as_data, Map.merge(audience, additional))
|
||||
{:ok, conversation, create_data}
|
||||
|
||||
nil ->
|
||||
{:error, :last_comment_not_found}
|
||||
end
|
||||
|
||||
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
end
|
||||
|
||||
nil ->
|
||||
{:error, :discussion_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
def create(args, additional) do
|
||||
with args when is_map(args) <- prepare_args(args) do
|
||||
case Conversations.create_conversation(args) do
|
||||
{:ok, %Conversation{} = conversation} ->
|
||||
ConversationActivity.insert_activity(conversation, subject: "conversation_created")
|
||||
conversation_as_data = Convertible.model_to_as(conversation)
|
||||
audience = Audience.get_audience(conversation)
|
||||
create_data = make_create_data(conversation_as_data, Map.merge(audience, additional))
|
||||
{:ok, conversation, create_data}
|
||||
|
||||
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Conversation.t(), map(), map()) ::
|
||||
{:ok, Conversation.t(), ActivityStream.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update(%Conversation{} = old_conversation, args, additional) do
|
||||
case Conversations.update_conversation(old_conversation, args) do
|
||||
{:ok, %Conversation{} = new_conversation} ->
|
||||
# ConversationActivity.insert_activity(new_conversation,
|
||||
# subject: "conversation_renamed",
|
||||
# old_conversation: old_conversation
|
||||
# )
|
||||
|
||||
conversation_as_data = Convertible.model_to_as(new_conversation)
|
||||
audience = Audience.get_audience(new_conversation)
|
||||
update_data = make_update_data(conversation_as_data, Map.merge(audience, additional))
|
||||
{:ok, new_conversation, update_data}
|
||||
|
||||
{:error, %Ecto.Changeset{} = err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Conversation.t(), Actor.t(), boolean, map()) ::
|
||||
{:error, Ecto.Changeset.t()} | {:ok, ActivityStream.t(), Actor.t(), Conversation.t()}
|
||||
def delete(
|
||||
%Conversation{} = _conversation,
|
||||
%Actor{} = _actor,
|
||||
_local,
|
||||
_additionnal
|
||||
) do
|
||||
{:error, :not_applicable}
|
||||
end
|
||||
|
||||
# @spec actor(Conversation.t()) :: Actor.t() | nil
|
||||
# def actor(%ConversationParticipant{actor_id: actor_id}), do: Actors.get_actor(actor_id)
|
||||
|
||||
# @spec group_actor(Conversation.t()) :: Actor.t() | nil
|
||||
# def group_actor(%Conversation{actor_id: actor_id}), do: Actors.get_actor(actor_id)
|
||||
|
||||
@spec permissions(Conversation.t()) :: Permission.t()
|
||||
def permissions(%Conversation{}) do
|
||||
%Permission{access: :member, create: :member, update: :moderator, delete: :moderator}
|
||||
end
|
||||
|
||||
@spec maybe_publish_graphql_subscription(Conversation.t()) :: :ok
|
||||
defp maybe_publish_graphql_subscription(%Conversation{} = conversation) do
|
||||
Absinthe.Subscription.publish(Endpoint, conversation,
|
||||
conversation_comment_changed: conversation.id
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec prepare_args(map) :: map | {:error, :empty_participants}
|
||||
defp prepare_args(args) do
|
||||
{text, mentions, _tags} =
|
||||
APIUtils.make_content_html(
|
||||
args |> Map.get(:text, "") |> String.trim(),
|
||||
# Can't put additional tags on a comment
|
||||
[],
|
||||
"text/html"
|
||||
)
|
||||
|
||||
mentions =
|
||||
(args |> Map.get(:mentions, []) |> prepare_mentions()) ++
|
||||
ConverterUtils.fetch_mentions(mentions)
|
||||
|
||||
if Enum.empty?(mentions) do
|
||||
{:error, :empty_participants}
|
||||
else
|
||||
event = Map.get(args, :event, get_event(Map.get(args, :event_id)))
|
||||
|
||||
participants =
|
||||
(mentions ++
|
||||
[
|
||||
%{actor_id: args.actor_id},
|
||||
%{
|
||||
actor_id:
|
||||
if(is_nil(event),
|
||||
do: nil,
|
||||
else: event.attributed_to_id || event.organizer_actor_id
|
||||
)
|
||||
}
|
||||
])
|
||||
|> Enum.reduce(
|
||||
[],
|
||||
fn %{actor_id: actor_id}, acc ->
|
||||
case Actors.get_actor(actor_id) do
|
||||
nil -> acc
|
||||
actor -> acc ++ [actor]
|
||||
end
|
||||
end
|
||||
)
|
||||
|> Enum.uniq_by(& &1.id)
|
||||
|
||||
args
|
||||
|> Map.put(:text, text)
|
||||
|> Map.put(:mentions, mentions)
|
||||
|> Map.put(:participants, participants)
|
||||
end
|
||||
end
|
||||
|
||||
@spec prepare_mentions(list(String.t())) :: list(%{actor_id: String.t()})
|
||||
defp prepare_mentions(mentions) do
|
||||
Enum.reduce(mentions, [], &prepare_mention/2)
|
||||
end
|
||||
|
||||
@spec prepare_mention(String.t() | map(), list()) :: list(%{actor_id: String.t()})
|
||||
defp prepare_mention(%{actor_id: _} = mention, mentions) do
|
||||
mentions ++ [mention]
|
||||
end
|
||||
|
||||
defp prepare_mention(mention, mentions) do
|
||||
case ActivityPubActor.find_or_make_actor_from_nickname(mention) do
|
||||
{:ok, %Actor{id: actor_id}} ->
|
||||
mentions ++ [%{actor_id: actor_id}]
|
||||
|
||||
{:error, _} ->
|
||||
mentions
|
||||
end
|
||||
end
|
||||
|
||||
defp get_event(nil), do: nil
|
||||
|
||||
defp get_event(event_id) do
|
||||
case Mobilizon.Events.get_event(event_id) do
|
||||
{:ok, event} -> event
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -22,6 +22,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
|
||||
@actor_types ["Group", "Person", "Application"]
|
||||
@all_actor_types @actor_types ++ ["Organization", "Service"]
|
||||
@ap_public_audience "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
# Wraps an object into an activity
|
||||
@spec create_activity(map(), boolean()) :: {:ok, Activity.t()}
|
||||
@@ -491,8 +492,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
if public do
|
||||
Logger.debug("Making announce data for a public object")
|
||||
|
||||
{[actor.followers_url, object_actor_url],
|
||||
["https://www.w3.org/ns/activitystreams#Public"]}
|
||||
{[actor.followers_url, object_actor_url], [@ap_public_audience]}
|
||||
else
|
||||
Logger.debug("Making announce data for a private object")
|
||||
|
||||
@@ -539,7 +539,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
"actor" => url,
|
||||
"object" => activity,
|
||||
"to" => [actor.followers_url, actor.url],
|
||||
"cc" => ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
"cc" => [@ap_public_audience]
|
||||
}
|
||||
|
||||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||||
|
||||
Reference in New Issue
Block a user