@@ -13,41 +13,38 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
alias Mobilizon.{
|
||||
Actors,
|
||||
Config,
|
||||
Conversations,
|
||||
Discussions,
|
||||
Events,
|
||||
Reports,
|
||||
Resources,
|
||||
Share,
|
||||
Todos,
|
||||
Users
|
||||
}
|
||||
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Reports.Report
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Todos.{Todo, TodoList}
|
||||
alias Mobilizon.Tombstone
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.{
|
||||
Activity,
|
||||
Audience,
|
||||
Federator,
|
||||
Fetcher,
|
||||
Preloader,
|
||||
Relay,
|
||||
Transmogrifier,
|
||||
Types,
|
||||
Visibility
|
||||
}
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Types.{Managable, Ownable}
|
||||
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Federation.HTTPSignatures.Signature
|
||||
alias Mobilizon.Federation.WebFinger
|
||||
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
alias Mobilizon.Service.Formatter.HTML
|
||||
alias Mobilizon.Service.Notifications.Scheduler
|
||||
alias Mobilizon.Service.RichMedia.Parser
|
||||
alias Mobilizon.Storage.Page
|
||||
|
||||
alias Mobilizon.Web.Endpoint
|
||||
alias Mobilizon.Web.Email.{Admin, Mailer}
|
||||
@@ -74,75 +71,44 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
Fetch an object from an URL, from our local database of events and comments, then eventually remote
|
||||
"""
|
||||
# TODO: Make database calls parallel
|
||||
@spec fetch_object_from_url(String.t()) :: {:ok, %Event{}} | {:ok, %Comment{}} | {:error, any()}
|
||||
def fetch_object_from_url(url) do
|
||||
@spec fetch_object_from_url(String.t(), Keyword.t()) ::
|
||||
{:ok, struct()} | {:error, any()}
|
||||
def fetch_object_from_url(url, options \\ []) do
|
||||
Logger.info("Fetching object from url #{url}")
|
||||
force_fetch = Keyword.get(options, :force, false)
|
||||
|
||||
with {:not_http, true} <- {:not_http, String.starts_with?(url, "http")},
|
||||
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(url)},
|
||||
{:existing_comment, nil} <- {:existing_comment, Conversations.get_comment_from_url(url)},
|
||||
{:existing_resource, nil} <- {:existing_resource, Resources.get_resource_by_url(url)},
|
||||
{:existing_actor, {:error, :actor_not_found}} <-
|
||||
{:existing_actor, Actors.get_actor_by_url(url)},
|
||||
date <- Signature.generate_date_header(),
|
||||
headers <-
|
||||
[{:Accept, "application/activity+json"}]
|
||||
|> maybe_date_fetch(date)
|
||||
|> sign_fetch_relay(url, date),
|
||||
{:ok, %HTTPoison.Response{body: body, status_code: code}} when code in 200..299 <-
|
||||
HTTPoison.get(
|
||||
url,
|
||||
headers,
|
||||
follow_redirect: true,
|
||||
timeout: 10_000,
|
||||
recv_timeout: 20_000,
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
),
|
||||
{:ok, data} <- Jason.decode(body),
|
||||
{:origin_check, true} <- {:origin_check, origin_check?(url, data)},
|
||||
params <- %{
|
||||
"type" => "Create",
|
||||
"to" => data["to"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["attributedTo"],
|
||||
"object" => data
|
||||
},
|
||||
{:ok, _activity, %{url: object_url} = _object} <- Transmogrifier.handle_incoming(params) do
|
||||
case data["type"] do
|
||||
"Event" ->
|
||||
{:ok, Events.get_public_event_by_url_with_preload!(object_url)}
|
||||
|
||||
"Note" ->
|
||||
{:ok, Conversations.get_comment_from_url_with_preload!(object_url)}
|
||||
|
||||
"Document" ->
|
||||
{:ok, Resources.get_resource_by_url_with_preloads(object_url)}
|
||||
|
||||
"ResourceCollection" ->
|
||||
{:ok, Resources.get_resource_by_url_with_preloads(object_url)}
|
||||
|
||||
"Actor" ->
|
||||
{:ok, Actors.get_actor_by_url!(object_url, true)}
|
||||
|
||||
other ->
|
||||
{:error, other}
|
||||
end
|
||||
{:existing, nil} <-
|
||||
{:existing, Tombstone.find_tombstone(url)},
|
||||
{:existing, nil} <- {:existing, Events.get_event_by_url(url)},
|
||||
{:existing, nil} <-
|
||||
{:existing, Discussions.get_discussion_by_url(url)},
|
||||
{:existing, nil} <- {:existing, Discussions.get_comment_from_url(url)},
|
||||
{:existing, nil} <- {:existing, Resources.get_resource_by_url(url)},
|
||||
{:existing, nil} <-
|
||||
{:existing, Actors.get_actor_by_url_2(url)},
|
||||
:ok <- Logger.info("Data for URL not found anywhere, going to fetch it"),
|
||||
{:ok, _activity, entity} <- Fetcher.fetch_and_create(url, options) do
|
||||
Logger.debug("Going to preload the new entity")
|
||||
Preloader.maybe_preload(entity)
|
||||
else
|
||||
{:existing_event, %Event{url: event_url}} ->
|
||||
{:ok, Events.get_public_event_by_url_with_preload!(event_url)}
|
||||
{:existing, entity} ->
|
||||
Logger.debug("Entity is already existing")
|
||||
|
||||
{:existing_comment, %Comment{url: comment_url}} ->
|
||||
{:ok, Conversations.get_comment_from_url_with_preload!(comment_url)}
|
||||
entity =
|
||||
if force_fetch and not compare_origins?(url, Endpoint.url()) do
|
||||
Logger.debug("Entity is external and we want a force fetch")
|
||||
|
||||
{:existing_resource, %Resource{url: resource_url}} ->
|
||||
{:ok, Resources.get_resource_by_url_with_preloads(resource_url)}
|
||||
with {:ok, _activity, entity} <- Fetcher.fetch_and_update(url, options) do
|
||||
entity
|
||||
end
|
||||
else
|
||||
entity
|
||||
end
|
||||
|
||||
{:existing_actor, {:ok, %Actor{url: actor_url}}} ->
|
||||
{:ok, Actors.get_actor_by_url!(actor_url, true)}
|
||||
Logger.debug("Going to preload an existing entity")
|
||||
|
||||
{:origin_check, false} ->
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, "Object origin check failed"}
|
||||
Preloader.maybe_preload(entity)
|
||||
|
||||
e ->
|
||||
Logger.warn("Something failed while fetching url #{inspect(e)}")
|
||||
@@ -201,15 +167,18 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
with {:tombstone, nil} <- {:tombstone, check_for_tombstones(args)},
|
||||
{:ok, entity, create_data} <-
|
||||
(case type do
|
||||
:event -> create_event(args, additional)
|
||||
:comment -> create_comment(args, additional)
|
||||
:group -> create_group(args, additional)
|
||||
:todo_list -> create_todo_list(args, additional)
|
||||
:todo -> create_todo(args, additional)
|
||||
:resource -> create_resource(args, additional)
|
||||
: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)
|
||||
end),
|
||||
{:ok, activity} <- create_activity(create_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
@@ -227,21 +196,15 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
* Federates (asynchronously) the activity
|
||||
* Returns the activity
|
||||
"""
|
||||
@spec update(atom(), struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
|
||||
def update(type, old_entity, args, local \\ false, additional \\ %{}) do
|
||||
@spec update(struct(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
|
||||
def update(old_entity, args, local \\ false, additional \\ %{}) do
|
||||
Logger.debug("updating an activity")
|
||||
Logger.debug(inspect(args))
|
||||
|
||||
with {:ok, entity, update_data} <-
|
||||
(case type do
|
||||
:event -> update_event(old_entity, args, additional)
|
||||
:comment -> update_comment(old_entity, args, additional)
|
||||
:actor -> update_actor(old_entity, args, additional)
|
||||
:todo -> update_todo(old_entity, args, additional)
|
||||
:resource -> update_resource(old_entity, args, additional)
|
||||
end),
|
||||
with {:ok, entity, update_data} <- Managable.update(old_entity, args, additional),
|
||||
{:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity) do
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
err ->
|
||||
@@ -366,182 +329,48 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
def delete(object, local \\ true)
|
||||
|
||||
@spec delete(Event.t(), boolean) :: {:ok, Activity.t(), Event.t()}
|
||||
def delete(%Event{url: url, organizer_actor: actor} = event, local) do
|
||||
data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
"object" => url,
|
||||
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"],
|
||||
"id" => url <> "/delete"
|
||||
}
|
||||
|
||||
with audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(event),
|
||||
{:ok, %Event{} = event} <- Events.delete_event(event),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}),
|
||||
Share.delete_all_by_uri(event.url),
|
||||
def delete(object, actor, local \\ true) do
|
||||
with {:ok, activity_data, actor, object} <-
|
||||
Managable.delete(object, actor, local),
|
||||
group <- Ownable.group_actor(object),
|
||||
:ok <- check_for_actor_key_rotation(actor),
|
||||
{:ok, activity} <- create_activity(Map.merge(data, audience), local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, event}
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity),
|
||||
:ok <- maybe_relay_if_group_activity(activity, group) do
|
||||
{:ok, activity, object}
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete(Comment.t(), boolean) :: {:ok, Activity.t(), Comment.t()}
|
||||
def delete(%Comment{url: url, actor: actor} = comment, local) do
|
||||
data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
"object" => url,
|
||||
"id" => url <> "/delete",
|
||||
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
with audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(comment),
|
||||
{:ok, %Comment{} = comment} <- Conversations.delete_comment(comment),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}),
|
||||
Share.delete_all_by_uri(comment.url),
|
||||
:ok <- check_for_actor_key_rotation(actor),
|
||||
{:ok, activity} <- create_activity(Map.merge(data, audience), local),
|
||||
def join(%Event{} = event, %Actor{} = actor, local \\ true, additional \\ %{}) do
|
||||
with {:ok, activity_data, participant} <- Types.Events.join(event, actor, local, additional),
|
||||
{:ok, activity} <- create_activity(activity_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, comment}
|
||||
end
|
||||
end
|
||||
|
||||
def delete(%Actor{url: url} = actor, local) do
|
||||
data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => url,
|
||||
"object" => url,
|
||||
"id" => url <> "/delete",
|
||||
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
# We completely delete the actor if activity is remote
|
||||
with {:ok, %Oban.Job{}} <- Actors.delete_actor(actor, reserve_username: local),
|
||||
{:ok, activity} <- create_activity(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, actor}
|
||||
end
|
||||
end
|
||||
|
||||
def delete(
|
||||
%Resource{url: url, actor: %Actor{url: actor_url}} = resource,
|
||||
local
|
||||
) do
|
||||
Logger.debug("Building Delete Resource activity")
|
||||
|
||||
data = %{
|
||||
"actor" => actor_url,
|
||||
"type" => "Delete",
|
||||
"object" => url,
|
||||
"id" => url <> "/delete",
|
||||
"to" => [actor_url]
|
||||
}
|
||||
|
||||
Logger.debug(inspect(data))
|
||||
|
||||
with {:ok, _resource} <- Resources.delete_resource(resource),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}"),
|
||||
{:ok, activity} <- create_activity(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity, resource}
|
||||
end
|
||||
end
|
||||
|
||||
def flag(args, local \\ false, _additional \\ %{}) do
|
||||
with {:build_args, args} <- {:build_args, prepare_args_for_report(args)},
|
||||
{:create_report, {:ok, %Report{} = report}} <-
|
||||
{:create_report, Reports.create_report(args)},
|
||||
report_as_data <- Convertible.model_to_as(report),
|
||||
cc <- if(local, do: [report.reported.url], else: []),
|
||||
report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}),
|
||||
{:ok, activity} <- create_activity(report_as_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
Enum.each(Users.list_moderators(), fn moderator ->
|
||||
moderator
|
||||
|> Admin.report(report)
|
||||
|> Mailer.deliver_later()
|
||||
end)
|
||||
|
||||
{:ok, activity, report}
|
||||
{:ok, activity, participant}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
{:maximum_attendee_capacity, err} ->
|
||||
{:maximum_attendee_capacity, err}
|
||||
|
||||
{:accept, accept} ->
|
||||
accept
|
||||
end
|
||||
end
|
||||
|
||||
def join(object, actor, local \\ true, additional \\ %{})
|
||||
|
||||
def join(%Event{} = event, %Actor{} = actor, local, additional) do
|
||||
# TODO Refactor me for federation
|
||||
with {:maximum_attendee_capacity, true} <-
|
||||
{:maximum_attendee_capacity, check_attendee_capacity(event)},
|
||||
role <-
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)),
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.create_participant(%{
|
||||
role: role,
|
||||
event_id: event.id,
|
||||
actor_id: actor.id,
|
||||
url: Map.get(additional, :url),
|
||||
metadata:
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
||||
def join_group(
|
||||
%{parent_id: parent_id, actor_id: actor_id, role: role},
|
||||
local \\ true,
|
||||
additional \\ %{}
|
||||
) do
|
||||
with {:ok, %Member{} = member} <-
|
||||
Mobilizon.Actors.create_member(%{
|
||||
parent_id: parent_id,
|
||||
actor_id: actor_id,
|
||||
role: role
|
||||
}),
|
||||
join_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(participant),
|
||||
{:ok, activity} <- create_activity(Map.merge(join_data, audience), local),
|
||||
activity_data when is_map(activity_data) <-
|
||||
Convertible.model_to_as(member),
|
||||
{:ok, activity} <- create_activity(Map.merge(activity_data, additional), local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
if event.local do
|
||||
cond do
|
||||
Mobilizon.Events.get_default_participant_role(event) === :participant &&
|
||||
role == :participant ->
|
||||
accept(
|
||||
:join,
|
||||
participant,
|
||||
true,
|
||||
%{"actor" => event.organizer_actor.url}
|
||||
)
|
||||
|
||||
Mobilizon.Events.get_default_participant_role(event) === :not_approved &&
|
||||
role == :not_approved ->
|
||||
Scheduler.pending_participation_notification(event)
|
||||
{:ok, activity, participant}
|
||||
|
||||
true ->
|
||||
{:ok, activity, participant}
|
||||
end
|
||||
else
|
||||
{:ok, activity, participant}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Implement me
|
||||
def join(%Actor{type: :Group} = _group, %Actor{} = _actor, _local, _additional) do
|
||||
:error
|
||||
end
|
||||
|
||||
defp check_attendee_capacity(%Event{options: options} = event) do
|
||||
with maximum_attendee_capacity <-
|
||||
Map.get(options, :maximum_attendee_capacity) || 0 do
|
||||
maximum_attendee_capacity == 0 ||
|
||||
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
|
||||
{:ok, activity, member}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -640,7 +469,7 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
|
||||
with {:ok, entity, update_data} <-
|
||||
(case type do
|
||||
:resource -> move_resource(old_entity, args, additional)
|
||||
:resource -> Types.Resources.move(old_entity, args, additional)
|
||||
end),
|
||||
{:ok, activity} <- create_activity(update_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
@@ -653,6 +482,25 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
end
|
||||
end
|
||||
|
||||
def flag(args, local \\ false, additional \\ %{}) do
|
||||
with {report, report_as_data} <- Types.Reports.flag(args, local, additional),
|
||||
{:ok, activity} <- create_activity(report_as_data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
Enum.each(Users.list_moderators(), fn moderator ->
|
||||
moderator
|
||||
|> Admin.report(report)
|
||||
|> Mailer.deliver_later()
|
||||
end)
|
||||
|
||||
{:ok, activity, report}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create an actor locally by its URL (AP ID)
|
||||
"""
|
||||
@@ -711,9 +559,29 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
defp is_create_activity?(%Activity{data: %{"type" => "Create"}}), do: true
|
||||
defp is_create_activity?(_), do: false
|
||||
|
||||
@spec is_announce_activity?(Activity.t()) :: boolean
|
||||
defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true
|
||||
defp is_announce_activity?(_), do: false
|
||||
@spec convert_members_in_recipients(list(String.t())) :: {list(String.t()), list(Actor.t())}
|
||||
defp convert_members_in_recipients(recipients) do
|
||||
Enum.reduce(recipients, {recipients, []}, fn recipient, {recipients, member_actors} = acc ->
|
||||
case Actors.get_group_by_members_url(recipient) do
|
||||
# If the group is local just add external members
|
||||
%Actor{domain: domain} = group when is_nil(domain) ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
|
||||
member_actors ++ Actors.list_external_actors_members_for_group(group)}
|
||||
|
||||
# If it's remote add the remote group actor as well
|
||||
%Actor{} = group ->
|
||||
{Enum.filter(recipients, fn recipient -> recipient != group.members_url end),
|
||||
member_actors ++ Actors.list_external_actors_members_for_group(group) ++ [group]}
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# @spec is_announce_activity?(Activity.t()) :: boolean
|
||||
# defp is_announce_activity?(%Activity{data: %{"type" => "Announce"}}), do: true
|
||||
# defp is_announce_activity?(_), do: false
|
||||
|
||||
@doc """
|
||||
Publish an activity to all appropriated audiences inboxes
|
||||
@@ -741,19 +609,11 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
{recipients, []}
|
||||
end
|
||||
|
||||
# If we want to send to all members of the group, because this server is the one the group is on
|
||||
{recipients, members} =
|
||||
if is_announce_activity?(activity) and actor.type == :Group and
|
||||
actor.members_url in activity.recipients and is_nil(actor.domain) do
|
||||
{Enum.filter(recipients, fn recipient -> recipient != actor.members_url end),
|
||||
Actors.list_external_members_for_group(actor)}
|
||||
else
|
||||
{recipients, []}
|
||||
end
|
||||
{recipients, members} = convert_members_in_recipients(recipients)
|
||||
|
||||
remote_inboxes =
|
||||
(remote_actors(recipients) ++ followers ++ members)
|
||||
|> Enum.map(fn follower -> follower.shared_inbox_url || follower.inbox_url end)
|
||||
|> Enum.map(fn actor -> actor.shared_inbox_url || actor.inbox_url end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
|
||||
@@ -791,16 +651,15 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
date: date
|
||||
})
|
||||
|
||||
HTTPoison.post(
|
||||
Tesla.post(
|
||||
inbox,
|
||||
json,
|
||||
[
|
||||
headers: [
|
||||
{"Content-Type", "application/activity+json"},
|
||||
{"signature", signature},
|
||||
{"digest", digest},
|
||||
{"date", date}
|
||||
],
|
||||
hackney: [pool: :default]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
@@ -811,18 +670,15 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
Logger.debug(inspect(url))
|
||||
|
||||
res =
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, [Accept: "application/activity+json"],
|
||||
follow_redirect: true,
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
),
|
||||
with {:ok, %{status: 200, body: body}} <-
|
||||
Tesla.get(url, headers: [{"Accept", "application/activity+json"}]),
|
||||
:ok <- Logger.debug("response okay, now decoding json"),
|
||||
{:ok, data} <- Jason.decode(body) do
|
||||
Logger.debug("Got activity+json response at actor's endpoint, now converting data")
|
||||
{:ok, Converter.Actor.as_to_model_data(data)}
|
||||
else
|
||||
# Actor is gone, probably deleted
|
||||
{:ok, %HTTPoison.Response{status_code: 410}} ->
|
||||
{:ok, %{status: 410}} ->
|
||||
Logger.info("Response HTTP 410")
|
||||
{:error, :actor_deleted}
|
||||
|
||||
@@ -839,10 +695,11 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
"""
|
||||
@spec fetch_public_activities_for_actor(Actor.t(), integer(), integer()) :: map()
|
||||
def fetch_public_activities_for_actor(%Actor{} = actor, page \\ 1, limit \\ 10) do
|
||||
{:ok, events, total_events} = Events.list_public_events_for_actor(actor, page, limit)
|
||||
%Page{total: total_events, elements: events} =
|
||||
Events.list_public_events_for_actor(actor, page, limit)
|
||||
|
||||
{:ok, comments, total_comments} =
|
||||
Conversations.list_public_comments_for_actor(actor, page, limit)
|
||||
%Page{total: total_comments, elements: comments} =
|
||||
Discussions.list_public_comments_for_actor(actor, page, limit)
|
||||
|
||||
event_activities = Enum.map(events, &event_to_activity/1)
|
||||
comment_activities = Enum.map(comments, &comment_to_activity/1)
|
||||
@@ -879,252 +736,10 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
Map.get(data, "to", []) ++ Map.get(data, "cc", [])
|
||||
end
|
||||
|
||||
@spec create_event(map(), map()) :: {:ok, map()}
|
||||
defp create_event(args, additional) do
|
||||
with args <- prepare_args_for_event(args),
|
||||
{:ok, %Event{} = event} <- Events.create_event(args),
|
||||
event_as_data <- Convertible.model_to_as(event),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(event),
|
||||
create_data <-
|
||||
make_create_data(event_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, event, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_comment(map(), map()) :: {:ok, map()}
|
||||
defp create_comment(args, additional) do
|
||||
with args <- prepare_args_for_comment(args),
|
||||
{:ok, %Comment{} = comment} <- Conversations.create_comment(args),
|
||||
comment_as_data <- Convertible.model_to_as(comment),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(comment),
|
||||
create_data <-
|
||||
make_create_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, comment, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_group(map(), map()) :: {:ok, map()}
|
||||
defp create_group(args, additional) do
|
||||
with args <- prepare_args_for_group(args),
|
||||
{:ok, %Actor{type: :Group} = group} <- Actors.create_group(args),
|
||||
group_as_data <- Convertible.model_to_as(group),
|
||||
audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(group_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, group, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_todo_list(map(), map()) :: {:ok, map()}
|
||||
defp create_todo_list(args, additional) do
|
||||
with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}),
|
||||
audience <- %{"to" => [group.url], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(todo_list_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, todo_list, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_todo(map(), map()) :: {:ok, map()}
|
||||
defp create_todo(args, additional) do
|
||||
with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <-
|
||||
Todos.create_todo(args),
|
||||
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
|
||||
%Actor{} = creator <- Actors.get_actor(creator_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator},
|
||||
todo_as_data <-
|
||||
Convertible.model_to_as(todo),
|
||||
audience <- %{"to" => [group.url], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(todo_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, todo, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
defp create_resource(%{type: type} = args, additional) do
|
||||
args =
|
||||
case type do
|
||||
:folder ->
|
||||
args
|
||||
|
||||
_ ->
|
||||
case Parser.parse(Map.get(args, :resource_url)) do
|
||||
{:ok, metadata} ->
|
||||
Map.put(args, :metadata, metadata)
|
||||
|
||||
_ ->
|
||||
args
|
||||
end
|
||||
end
|
||||
|
||||
with {:ok,
|
||||
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <-
|
||||
Resources.create_resource(args),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group, creator: creator}),
|
||||
audience <- %{
|
||||
"to" => [group.url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
} do
|
||||
create_data =
|
||||
case parent_id do
|
||||
nil ->
|
||||
make_create_data(resource_as_data, Map.merge(audience, additional))
|
||||
|
||||
parent_id ->
|
||||
# In case the resource has a parent we don't `Create` the resource but `Add` it to an existing resource
|
||||
parent = Resources.get_resource(parent_id)
|
||||
make_add_data(resource_as_data, parent, Map.merge(audience, additional))
|
||||
end
|
||||
|
||||
{:ok, resource, create_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec check_for_tombstones(map()) :: Tombstone.t() | nil
|
||||
defp check_for_tombstones(%{url: url}), do: Tombstone.find_tombstone(url)
|
||||
defp check_for_tombstones(_), do: nil
|
||||
|
||||
@spec update_event(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any()
|
||||
defp update_event(%Event{} = old_event, args, additional) do
|
||||
with args <- prepare_args_for_event(args),
|
||||
{:ok, %Event{} = new_event} <- Events.update_event(old_event, args),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"),
|
||||
event_as_data <- Convertible.model_to_as(new_event),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(new_event),
|
||||
update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_event, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_comment(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any()
|
||||
defp update_comment(%Comment{} = old_comment, args, additional) do
|
||||
with args <- prepare_args_for_comment(args),
|
||||
{:ok, %Comment{} = new_comment} <- Conversations.update_comment(old_comment, args),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
|
||||
comment_as_data <- Convertible.model_to_as(new_comment),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(new_comment),
|
||||
update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_comment, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_actor(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any
|
||||
defp update_actor(%Actor{} = old_actor, args, additional) do
|
||||
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
|
||||
actor_as_data <- Convertible.model_to_as(new_actor),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(new_actor),
|
||||
additional <- Map.merge(additional, %{"actor" => old_actor.url}),
|
||||
update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_actor, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_todo(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any
|
||||
defp update_todo(%Todo{} = old_todo, args, additional) do
|
||||
with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args),
|
||||
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_as_data <-
|
||||
Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}),
|
||||
audience <- %{"to" => [group.url], "cc" => []},
|
||||
update_data <-
|
||||
make_update_data(todo_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, todo, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
defp update_resource(%Resource{} = old_resource, %{parent_id: _parent_id} = args, additional) do
|
||||
move_resource(old_resource, args, additional)
|
||||
end
|
||||
|
||||
# Simple rename
|
||||
defp update_resource(%Resource{} = old_resource, %{title: title} = _args, additional) do
|
||||
with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <-
|
||||
Resources.update_resource(old_resource, %{title: title}),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group}),
|
||||
audience <- %{
|
||||
"to" => [group.url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
},
|
||||
update_data <-
|
||||
make_update_data(resource_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, resource, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
defp move_resource(
|
||||
%Resource{parent_id: old_parent_id} = old_resource,
|
||||
%{parent_id: _new_parent_id} = args,
|
||||
additional
|
||||
) do
|
||||
with {:ok,
|
||||
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} =
|
||||
resource} <-
|
||||
Resources.update_resource(old_resource, args),
|
||||
old_parent <- Resources.get_resource(old_parent_id),
|
||||
new_parent <- Resources.get_resource(new_parent_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group}),
|
||||
audience <- %{
|
||||
"to" => [group.url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
},
|
||||
move_data <-
|
||||
make_move_data(
|
||||
resource_as_data,
|
||||
old_parent,
|
||||
new_parent,
|
||||
Map.merge(audience, additional)
|
||||
) do
|
||||
{:ok, resource, move_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@spec accept_follow(Follower.t(), map) :: {:ok, Follower.t(), Activity.t()} | any
|
||||
defp accept_follow(%Follower{} = follower, additional) do
|
||||
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, %{approved: true}),
|
||||
@@ -1254,138 +869,4 @@ defmodule Mobilizon.Federation.ActivityPub do
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
# Prepare and sanitize arguments for events
|
||||
defp prepare_args_for_event(args) do
|
||||
# If title is not set: we are not updating it
|
||||
args =
|
||||
if Map.has_key?(args, :title) && !is_nil(args.title),
|
||||
do: Map.update(args, :title, "", &String.trim/1),
|
||||
else: args
|
||||
|
||||
# If we've been given a description (we might not get one if updating)
|
||||
# sanitize it, HTML it, and extract tags & mentions from it
|
||||
args =
|
||||
if Map.has_key?(args, :description) && !is_nil(args.description) do
|
||||
{description, mentions, tags} =
|
||||
APIUtils.make_content_html(
|
||||
String.trim(args.description),
|
||||
Map.get(args, :tags, []),
|
||||
"text/html"
|
||||
)
|
||||
|
||||
mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions)
|
||||
|
||||
Map.merge(args, %{
|
||||
description: description,
|
||||
mentions: mentions,
|
||||
tags: tags
|
||||
})
|
||||
else
|
||||
args
|
||||
end
|
||||
|
||||
# Check that we can only allow anonymous participation if our instance allows it
|
||||
{_, options} =
|
||||
Map.get_and_update(
|
||||
Map.get(args, :options, %{anonymous_participation: false}),
|
||||
:anonymous_participation,
|
||||
fn value ->
|
||||
{value, value && Mobilizon.Config.anonymous_participation?()}
|
||||
end
|
||||
)
|
||||
|
||||
args = Map.put(args, :options, options)
|
||||
|
||||
Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1)
|
||||
end
|
||||
|
||||
# Prepare and sanitize arguments for comments
|
||||
defp prepare_args_for_comment(args) do
|
||||
with in_reply_to_comment <-
|
||||
args |> Map.get(:in_reply_to_comment_id) |> Conversations.get_comment_with_preload(),
|
||||
event <- args |> Map.get(:event_id) |> handle_event_for_comment(),
|
||||
args <- Map.update(args, :visibility, :public, & &1),
|
||||
{text, mentions, tags} <-
|
||||
APIUtils.make_content_html(
|
||||
args |> Map.get(:text, "") |> String.trim(),
|
||||
# Can't put additional tags on a comment
|
||||
[],
|
||||
"text/html"
|
||||
),
|
||||
tags <- ConverterUtils.fetch_tags(tags),
|
||||
mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions),
|
||||
args <-
|
||||
Map.merge(args, %{
|
||||
actor_id: Map.get(args, :actor_id),
|
||||
text: text,
|
||||
mentions: mentions,
|
||||
tags: tags,
|
||||
event: event,
|
||||
in_reply_to_comment: in_reply_to_comment,
|
||||
in_reply_to_comment_id:
|
||||
if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id)),
|
||||
origin_comment_id:
|
||||
if(is_nil(in_reply_to_comment),
|
||||
do: nil,
|
||||
else: Comment.get_thread_id(in_reply_to_comment)
|
||||
)
|
||||
}) do
|
||||
args
|
||||
end
|
||||
end
|
||||
|
||||
@spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil
|
||||
defp handle_event_for_comment(event_id) when not is_nil(event_id) do
|
||||
case Events.get_event_with_preload(event_id) do
|
||||
{:ok, %Event{} = event} -> event
|
||||
{:error, :event_not_found} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_event_for_comment(nil), do: nil
|
||||
|
||||
defp prepare_args_for_group(args) do
|
||||
with preferred_username <-
|
||||
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
|
||||
summary <- args |> Map.get(:summary, "") |> String.trim(),
|
||||
{summary, _mentions, _tags} <-
|
||||
summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do
|
||||
%{args | preferred_username: preferred_username, summary: summary}
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_args_for_report(args) do
|
||||
with {:reporter, %Actor{} = reporter_actor} <-
|
||||
{:reporter, Actors.get_actor!(args.reporter_id)},
|
||||
{:reported, %Actor{} = reported_actor} <-
|
||||
{:reported, Actors.get_actor!(args.reported_id)},
|
||||
content <- HTML.strip_tags(args.content),
|
||||
event <- Conversations.get_comment(Map.get(args, :event_id)),
|
||||
{:get_report_comments, comments} <-
|
||||
{:get_report_comments,
|
||||
Conversations.list_comments_by_actor_and_ids(
|
||||
reported_actor.id,
|
||||
Map.get(args, :comments_ids, [])
|
||||
)} do
|
||||
Map.merge(args, %{
|
||||
reporter: reporter_actor,
|
||||
reported: reported_actor,
|
||||
content: content,
|
||||
event: event,
|
||||
comments: comments
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
defp check_for_actor_key_rotation(%Actor{} = actor) do
|
||||
if Actors.should_rotate_actor_key(actor) do
|
||||
Actors.schedule_key_rotation(
|
||||
actor,
|
||||
Application.get_env(:mobilizon, :activitypub)[:actor_key_rotation_delay]
|
||||
)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Share
|
||||
alias Mobilizon.Storage.Repo
|
||||
@@ -79,6 +79,14 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||
|
||||
def get_addressed_actors(mentioned_users, _), do: mentioned_users
|
||||
|
||||
def calculate_to_and_cc_from_mentions(
|
||||
%Comment{discussion: %Discussion{actor_id: actor_id}} = _comment
|
||||
) do
|
||||
with %Actor{type: :Group, members_url: members_url} <- Actors.get_actor(actor_id) do
|
||||
%{"to" => [members_url], "cc" => []}
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_to_and_cc_from_mentions(%Comment{} = comment) do
|
||||
with mentioned_actors <- Enum.map(comment.mentions, &process_mention/1),
|
||||
addressed_actors <- get_addressed_actors(mentioned_actors, nil),
|
||||
@@ -96,6 +104,28 @@ defmodule Mobilizon.Federation.ActivityPub.Audience do
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_to_and_cc_from_mentions(%Discussion{actor_id: actor_id}) do
|
||||
with %Actor{type: :Group, members_url: members_url} <- Actors.get_actor(actor_id) do
|
||||
%{"to" => [members_url], "cc" => []}
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_to_and_cc_from_mentions(%Event{
|
||||
attributed_to: %Actor{members_url: members_url},
|
||||
visibility: visibility
|
||||
}) do
|
||||
case visibility do
|
||||
:public ->
|
||||
%{"to" => [members_url, @ap_public], "cc" => []}
|
||||
|
||||
:unlisted ->
|
||||
%{"to" => [members_url], "cc" => [@ap_public]}
|
||||
|
||||
:private ->
|
||||
%{"to" => [members_url], "cc" => []}
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_to_and_cc_from_mentions(%Event{} = event) do
|
||||
with mentioned_actors <- Enum.map(event.mentions, &process_mention/1),
|
||||
addressed_actors <- get_addressed_actors(mentioned_actors, nil),
|
||||
|
||||
74
lib/federation/activity_pub/fetcher.ex
Normal file
74
lib/federation/activity_pub/fetcher.ex
Normal file
@@ -0,0 +1,74 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Fetcher do
|
||||
@moduledoc """
|
||||
Module to handle direct URL ActivityPub fetches to remote content
|
||||
|
||||
If you need to first get cached data, see `Mobilizon.Federation.ActivityPub.fetch_object_from_url/2`
|
||||
"""
|
||||
require Logger
|
||||
|
||||
alias Mobilizon.Federation.HTTPSignatures.Signature
|
||||
alias Mobilizon.Federation.ActivityPub.{Relay, Transmogrifier}
|
||||
alias Mobilizon.Service.HTTP.ActivityPub, as: ActivityPubClient
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [maybe_date_fetch: 2, sign_fetch: 4, origin_check?: 2]
|
||||
|
||||
@spec fetch(String.t(), Keyword.t()) :: {:ok, map()}
|
||||
def fetch(url, options \\ []) do
|
||||
on_behalf_of = Keyword.get(options, :on_behalf_of, Relay.get_actor())
|
||||
|
||||
with date <- Signature.generate_date_header(),
|
||||
headers <-
|
||||
[{:Accept, "application/activity+json"}]
|
||||
|> maybe_date_fetch(date)
|
||||
|> sign_fetch(on_behalf_of, url, date),
|
||||
client <-
|
||||
ActivityPubClient.client(headers: headers),
|
||||
{:ok, %Tesla.Env{body: data, status: code}} when code in 200..299 <-
|
||||
ActivityPubClient.get(client, url) do
|
||||
{:ok, data}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_and_create(String.t(), Keyword.t()) :: {:ok, map(), struct()}
|
||||
def fetch_and_create(url, options \\ []) do
|
||||
with {:ok, data} when is_map(data) <- fetch(url, options),
|
||||
:ok <- Logger.debug("inspect body from fetch_object_from_url #{url}"),
|
||||
:ok <- Logger.debug(inspect(data)),
|
||||
{:origin_check, true} <- {:origin_check, origin_check?(url, data)},
|
||||
params <- %{
|
||||
"type" => "Create",
|
||||
"to" => data["to"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["attributedTo"] || data["actor"],
|
||||
"object" => data
|
||||
} do
|
||||
Transmogrifier.handle_incoming(params)
|
||||
else
|
||||
{:origin_check, false} ->
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, "Object origin check failed"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_and_update(String.t(), Keyword.t()) :: {:ok, map(), struct()}
|
||||
def fetch_and_update(url, options \\ []) do
|
||||
with {:ok, data} when is_map(data) <- fetch(url, options),
|
||||
:ok <- Logger.debug("inspect body from fetch_object_from_url #{url}"),
|
||||
:ok <- Logger.debug(inspect(data)),
|
||||
{:origin_check, true} <- {:origin_check, origin_check?(url, data)},
|
||||
params <- %{
|
||||
"type" => "Update",
|
||||
"to" => data["to"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["attributedTo"] || data["actor"],
|
||||
"object" => data
|
||||
} do
|
||||
Transmogrifier.handle_incoming(params)
|
||||
else
|
||||
{:origin_check, false} ->
|
||||
Logger.warn("Object origin check failed")
|
||||
{:error, "Object origin check failed"}
|
||||
end
|
||||
end
|
||||
end
|
||||
30
lib/federation/activity_pub/preloader.ex
Normal file
30
lib/federation/activity_pub/preloader.ex
Normal file
@@ -0,0 +1,30 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Preloader do
|
||||
@moduledoc """
|
||||
Module to ensure entities are correctly preloaded
|
||||
"""
|
||||
|
||||
# TODO: Move me in a more appropriate place
|
||||
alias Mobilizon.{Actors, Discussions, Events, Resources}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Tombstone
|
||||
|
||||
def maybe_preload(%Event{url: url}),
|
||||
do: {:ok, Events.get_public_event_by_url_with_preload!(url)}
|
||||
|
||||
def maybe_preload(%Comment{url: url}),
|
||||
do: {:ok, Discussions.get_comment_from_url_with_preload!(url)}
|
||||
|
||||
def maybe_preload(%Discussion{} = discussion), do: {:ok, discussion}
|
||||
|
||||
def maybe_preload(%Resource{url: url}),
|
||||
do: {:ok, Resources.get_resource_by_url_with_preloads(url)}
|
||||
|
||||
def maybe_preload(%Actor{url: url}), do: {:ok, Actors.get_actor_by_url!(url, true)}
|
||||
|
||||
def maybe_preload(%Tombstone{uri: _uri} = tombstone), do: {:ok, tombstone}
|
||||
|
||||
def maybe_preload(other), do: {:error, other}
|
||||
end
|
||||
@@ -3,24 +3,31 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
Module that provides functions to explore and fetch collections on a group
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Member, as: MemberConverter
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Resource, as: ResourceConverter
|
||||
alias Mobilizon.Federation.HTTPSignatures.Signature
|
||||
alias Mobilizon.Resources
|
||||
alias Mobilizon.Federation.ActivityPub.{Fetcher, Transmogrifier}
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [maybe_date_fetch: 2, sign_fetch: 4]
|
||||
|
||||
@spec fetch_group(String.t(), Actor.t()) :: :ok
|
||||
def fetch_group(group_url, %Actor{} = on_behalf_of) do
|
||||
with {:ok, %Actor{resources_url: resources_url, members_url: members_url}} <-
|
||||
with {:ok,
|
||||
%Actor{
|
||||
outbox_url: outbox_url,
|
||||
resources_url: resources_url,
|
||||
members_url: members_url,
|
||||
posts_url: posts_url,
|
||||
todos_url: todos_url,
|
||||
discussions_url: discussions_url,
|
||||
events_url: events_url
|
||||
}} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(group_url) do
|
||||
fetch_collection(outbox_url, on_behalf_of)
|
||||
fetch_collection(members_url, on_behalf_of)
|
||||
fetch_collection(resources_url, on_behalf_of)
|
||||
fetch_collection(posts_url, on_behalf_of)
|
||||
fetch_collection(todos_url, on_behalf_of)
|
||||
fetch_collection(discussions_url, on_behalf_of)
|
||||
fetch_collection(events_url, on_behalf_of)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,12 +37,28 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
Logger.debug("Fetching and preparing collection from url")
|
||||
Logger.debug(inspect(collection_url))
|
||||
|
||||
with {:ok, data} <- fetch(collection_url, on_behalf_of) do
|
||||
with {:ok, data} <- Fetcher.fetch(collection_url, on_behalf_of: on_behalf_of) do
|
||||
Logger.debug("Fetch ok, passing to process_collection")
|
||||
process_collection(data, on_behalf_of)
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_element(String.t(), Actor.t()) :: any()
|
||||
def fetch_element(url, %Actor{} = on_behalf_of) do
|
||||
with {:ok, data} <- Fetcher.fetch(url, on_behalf_of: on_behalf_of) do
|
||||
case handling_element(data) do
|
||||
{:ok, _activity, entity} ->
|
||||
{:ok, entity}
|
||||
|
||||
{:ok, entity} ->
|
||||
{:ok, entity}
|
||||
|
||||
err ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp process_collection(%{"type" => type, "orderedItems" => items}, _on_behalf_of)
|
||||
when type in ["OrderedCollection", "OrderedCollectionPage"] do
|
||||
Logger.debug(
|
||||
@@ -55,55 +78,26 @@ defmodule Mobilizon.Federation.ActivityPub.Refresher do
|
||||
when is_bitstring(first) do
|
||||
Logger.debug("OrderedCollection has a first property pointing to an URI")
|
||||
|
||||
with {:ok, data} <- fetch(first, on_behalf_of) do
|
||||
with {:ok, data} <- Fetcher.fetch(first, on_behalf_of: on_behalf_of) do
|
||||
Logger.debug("Fetched the collection for first property")
|
||||
process_collection(data, on_behalf_of)
|
||||
end
|
||||
end
|
||||
|
||||
defp handling_element(%{"type" => "Member"} = data) do
|
||||
Logger.debug("Handling Member element")
|
||||
defp handling_element(data) when is_map(data) do
|
||||
activity = %{
|
||||
"type" => "Create",
|
||||
"to" => data["to"],
|
||||
"cc" => data["cc"],
|
||||
"actor" => data["actor"],
|
||||
"attributedTo" => data["attributedTo"],
|
||||
"object" => data
|
||||
}
|
||||
|
||||
data
|
||||
|> MemberConverter.as_to_model_data()
|
||||
|> Actors.create_member()
|
||||
Transmogrifier.handle_incoming(activity)
|
||||
end
|
||||
|
||||
defp handling_element(%{"type" => type} = data)
|
||||
when type in ["Document", "ResourceCollection"] do
|
||||
Logger.debug("Handling Resource element")
|
||||
|
||||
data
|
||||
|> ResourceConverter.as_to_model_data()
|
||||
|> Resources.create_resource()
|
||||
end
|
||||
|
||||
defp fetch(url, %Actor{} = on_behalf_of) do
|
||||
with date <- Signature.generate_date_header(),
|
||||
headers <-
|
||||
[{:Accept, "application/activity+json"}]
|
||||
|> maybe_date_fetch(date)
|
||||
|> sign_fetch(on_behalf_of, url, date),
|
||||
%HTTPoison.Response{status_code: 200, body: body} <-
|
||||
HTTPoison.get!(url, headers,
|
||||
follow_redirect: true,
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
),
|
||||
{:ok, data} <-
|
||||
Jason.decode(body) do
|
||||
{:ok, data}
|
||||
else
|
||||
# Actor is gone, probably deleted
|
||||
{:ok, %HTTPoison.Response{status_code: 410}} ->
|
||||
Logger.info("Response HTTP 410")
|
||||
{:error, :actor_deleted}
|
||||
|
||||
{:origin_check, false} ->
|
||||
{:error, "Origin check failed"}
|
||||
|
||||
e ->
|
||||
Logger.warn("Could not decode actor at fetch #{url}, #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
defp handling_element(uri) when is_binary(uri) do
|
||||
ActivityPub.fetch_object_from_url(uri)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,17 +8,19 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
A module to handle coding from internal to wire ActivityPub and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Conversations, Events, Resources, Todos}
|
||||
alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos}
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Todos.{Todo, TodoList}
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Relay, Utils}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Ownable
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
|
||||
alias Mobilizon.Tombstone
|
||||
alias Mobilizon.Web.Email.{Group, Participation}
|
||||
|
||||
require Logger
|
||||
@@ -62,10 +64,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
with object_data when is_map(object_data) <-
|
||||
object |> Converter.Comment.as_to_model_data(),
|
||||
{:existing_comment, {:error, :comment_not_found}} <-
|
||||
{:existing_comment, Conversations.get_comment_from_url_with_preload(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Comment{} = comment} <-
|
||||
ActivityPub.create(:comment, object_data, false) do
|
||||
{:ok, activity, comment}
|
||||
{:existing_comment, Discussions.get_comment_from_url_with_preload(object_data.url)},
|
||||
object_data <- transform_object_data_for_discussion(object_data) do
|
||||
# Check should be better
|
||||
|
||||
{:ok, %Activity{} = activity, entity} =
|
||||
if is_data_for_comment_or_discussion?(object_data) do
|
||||
Logger.debug("Chosing to create a regular comment")
|
||||
ActivityPub.create(:comment, object_data, false)
|
||||
else
|
||||
Logger.debug("Chosing to initialize or add a comment to a conversation")
|
||||
ActivityPub.create(:discussion, object_data, false)
|
||||
end
|
||||
|
||||
{:ok, activity, entity}
|
||||
else
|
||||
{:existing_comment, {:ok, %Comment{} = comment}} ->
|
||||
{:ok, nil, comment}
|
||||
@@ -100,6 +112,77 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(%{
|
||||
"type" => "Create",
|
||||
"object" => %{"type" => "Group", "id" => group_url} = _object
|
||||
}) do
|
||||
Logger.info("Handle incoming to create a group")
|
||||
|
||||
with {:ok, %Actor{} = group} <- ActivityPub.get_or_fetch_actor_by_url(group_url) do
|
||||
{:ok, nil, group}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(%{
|
||||
"type" => "Create",
|
||||
"object" => %{"type" => "Member"} = object
|
||||
}) do
|
||||
Logger.info("Handle incoming to create a member")
|
||||
|
||||
with object_data when is_map(object_data) <-
|
||||
object |> Converter.Member.as_to_model_data(),
|
||||
{:existing_member, nil} <-
|
||||
{:existing_member, Actors.get_member_by_url(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Member{} = member} <-
|
||||
ActivityPub.join_group(object_data, false) do
|
||||
{:ok, activity, member}
|
||||
else
|
||||
{:existing_member, %Member{} = member} ->
|
||||
{:ok, nil, member}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(%{
|
||||
"type" => "Create",
|
||||
"object" =>
|
||||
%{"type" => "Article", "actor" => _actor, "attributedTo" => _attributed_to} = object
|
||||
}) do
|
||||
Logger.info("Handle incoming to create articles")
|
||||
|
||||
with object_data when is_map(object_data) <-
|
||||
object |> Converter.Post.as_to_model_data(),
|
||||
{:existing_post, nil} <-
|
||||
{:existing_post, Posts.get_post_by_url(object_data.url)},
|
||||
{:ok, %Activity{} = activity, %Post{} = post} <-
|
||||
ActivityPub.create(:post, object_data, false) do
|
||||
{:ok, activity, post}
|
||||
else
|
||||
{:existing_post, %Post{} = post} ->
|
||||
{:ok, nil, post}
|
||||
end
|
||||
end
|
||||
|
||||
# This is a hack to handle Tombstones fetched by AP
|
||||
def handle_incoming(%{
|
||||
"type" => "Create",
|
||||
"object" => %{"type" => "Tombstone", "id" => object_url} = _object
|
||||
}) do
|
||||
Logger.info("Handle incoming to create a tombstone")
|
||||
|
||||
case ActivityPub.fetch_object_from_url(object_url, force: true) do
|
||||
# We already have the tombstone, object is probably already deleted
|
||||
{:ok, %Tombstone{} = tombstone} ->
|
||||
{:ok, nil, tombstone}
|
||||
|
||||
# Hack because deleted comments
|
||||
{:ok, %Comment{deleted_at: deleted_at} = comment} when not is_nil(deleted_at) ->
|
||||
{:ok, nil, comment}
|
||||
|
||||
{:ok, entity} ->
|
||||
ActivityPub.delete(entity, Relay.get_actor(), false)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
|
||||
) do
|
||||
@@ -165,7 +248,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
Logger.info("Handle incoming to create a resource")
|
||||
Logger.debug(inspect(data))
|
||||
|
||||
group_url = hd(to)
|
||||
group_url = if is_list(to) and not is_nil(to), do: hd(to), else: to
|
||||
|
||||
with {:existing_resource, nil} <-
|
||||
{:existing_resource, Resources.get_resource_by_url(object_url)},
|
||||
@@ -175,8 +258,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
{:member, Actors.is_member?(object_data.creator_id, object_data.actor_id)},
|
||||
{:ok, %Activity{} = activity, %Resource{} = resource} <-
|
||||
ActivityPub.create(:resource, object_data, false),
|
||||
{:ok, %Actor{type: :Group, id: group_id} = group} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(group_url),
|
||||
%Actor{type: :Group, id: group_id} = group <-
|
||||
Actors.get_group_by_members_url(group_url),
|
||||
announce_id <- "#{object_url}/announces/#{group_id}",
|
||||
{:ok, _activity, _resource} <- ActivityPub.announce(group, object, announce_id) do
|
||||
{:ok, activity, resource}
|
||||
@@ -190,7 +273,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
:error
|
||||
|
||||
{:error, e} ->
|
||||
Logger.error(inspect(e))
|
||||
Logger.debug(inspect(e))
|
||||
:error
|
||||
end
|
||||
end
|
||||
@@ -261,23 +344,14 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
def handle_incoming(
|
||||
%{"type" => "Announce", "object" => object, "actor" => _actor, "id" => _id} = data
|
||||
) do
|
||||
with actor <- Utils.get_actor(data),
|
||||
# TODO: Is the following line useful?
|
||||
{:ok, %Actor{id: actor_id, suspended: false} = _actor} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(actor),
|
||||
with actor_url <- Utils.get_actor(data),
|
||||
{:ok, %Actor{id: actor_id, suspended: false} = actor} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(actor_url),
|
||||
:ok <- Logger.debug("Fetching contained object"),
|
||||
{:ok, object} <- fetch_obj_helper_as_activity_streams(object),
|
||||
:ok <- Logger.debug("Handling contained object"),
|
||||
create_data <- Utils.make_create_data(object),
|
||||
:ok <- Logger.debug(inspect(object)),
|
||||
{:ok, _activity, entity} <- handle_incoming(create_data),
|
||||
:ok <- Logger.debug("Finished processing contained object"),
|
||||
{:ok, activity} <- ActivityPub.create_activity(data, false),
|
||||
{:ok, %Actor{id: object_owner_actor_id}} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(object["actor"]),
|
||||
{:ok, %Mobilizon.Share{} = _share} <-
|
||||
Mobilizon.Share.create(object["id"], actor_id, object_owner_actor_id) do
|
||||
{:ok, activity, entity}
|
||||
{:ok, entity} <-
|
||||
object |> Utils.get_url() |> fetch_object_optionnally_authenticated(actor),
|
||||
:ok <- eventually_create_share(object, entity, actor_id) do
|
||||
{:ok, nil, entity}
|
||||
else
|
||||
e ->
|
||||
Logger.debug(inspect(e))
|
||||
@@ -296,7 +370,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
object_data <-
|
||||
object |> Converter.Actor.as_to_model_data(),
|
||||
{:ok, %Activity{} = activity, %Actor{} = new_actor} <-
|
||||
ActivityPub.update(:actor, old_actor, object_data, false) do
|
||||
ActivityPub.update(old_actor, object_data, false) do
|
||||
{:ok, activity, new_actor}
|
||||
else
|
||||
e ->
|
||||
@@ -317,7 +391,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
object_data <- Converter.Event.as_to_model_data(object),
|
||||
{:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)},
|
||||
{:ok, %Activity{} = activity, %Event{} = new_event} <-
|
||||
ActivityPub.update(:event, old_event, object_data, false) do
|
||||
ActivityPub.update(old_event, object_data, false) do
|
||||
{:ok, activity, new_event}
|
||||
else
|
||||
_e ->
|
||||
@@ -325,6 +399,42 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Update", "object" => %{"type" => "Note"} = object, "actor" => _actor} =
|
||||
update_data
|
||||
) do
|
||||
with actor <- Utils.get_actor(update_data),
|
||||
{:ok, %Actor{url: actor_url, suspended: false}} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(actor),
|
||||
{:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)},
|
||||
object_data <- Converter.Comment.as_to_model_data(object),
|
||||
{:ok, old_entity} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
|
||||
object_data <- transform_object_data_for_discussion(object_data),
|
||||
{:ok, %Activity{} = activity, new_entity} <-
|
||||
ActivityPub.update(old_entity, object_data, false) do
|
||||
{:ok, activity, new_entity}
|
||||
else
|
||||
_e ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(%{
|
||||
"type" => "Update",
|
||||
"object" => %{"type" => "Tombstone"} = object,
|
||||
"actor" => _actor
|
||||
}) do
|
||||
Logger.info("Handle incoming to update a tombstone")
|
||||
|
||||
with object_url <- Utils.get_url(object),
|
||||
{:ok, entity} <- ActivityPub.fetch_object_from_url(object_url) do
|
||||
ActivityPub.delete(entity, Relay.get_actor(), false)
|
||||
else
|
||||
{:ok, %Tombstone{} = tombstone} ->
|
||||
{:ok, nil, tombstone}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{
|
||||
"type" => "Undo",
|
||||
@@ -367,21 +477,20 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: We presently assume that any actor on the same origin domain as the object being
|
||||
# deleted has the rights to delete that object. A better way to validate whether or not
|
||||
# the object should be deleted is to refetch the object URI, which should return either
|
||||
# an error or a tombstone. This would allow us to verify that a deletion actually took
|
||||
# place.
|
||||
# We assume everyone on the same instance as the object
|
||||
# or who is member of a group has the right to delete the object
|
||||
def handle_incoming(
|
||||
%{"type" => "Delete", "object" => object, "actor" => _actor, "id" => _id} = data
|
||||
) do
|
||||
with actor <- Utils.get_actor(data),
|
||||
{:ok, %Actor{url: actor_url}} <- ActivityPub.get_or_fetch_actor_by_url(actor),
|
||||
with actor_url <- Utils.get_actor(data),
|
||||
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
|
||||
object_id <- Utils.get_url(object),
|
||||
{:origin_check, true} <-
|
||||
{:origin_check, Utils.origin_check_from_id?(actor_url, object_id)},
|
||||
{:ok, object} <- ActivityPub.fetch_object_from_url(object_id),
|
||||
{:ok, activity, object} <- ActivityPub.delete(object, false) do
|
||||
{:origin_check, true} <-
|
||||
{:origin_check,
|
||||
Utils.origin_check_from_id?(actor_url, object_id) ||
|
||||
Utils.activity_actor_is_group_member?(actor, object)},
|
||||
{:ok, activity, object} <- ActivityPub.delete(object, actor, false) do
|
||||
{:ok, activity, object}
|
||||
else
|
||||
{:origin_check, false} ->
|
||||
@@ -449,6 +558,8 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
"target" => target
|
||||
} = data
|
||||
) do
|
||||
Logger.info("Handle incoming to invite someone")
|
||||
|
||||
with {:ok, %Actor{} = actor} <-
|
||||
data |> Utils.get_actor() |> ActivityPub.get_or_fetch_actor_by_url(),
|
||||
{:ok, object} <- object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
|
||||
@@ -485,7 +596,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
# end
|
||||
|
||||
def handle_incoming(object) do
|
||||
Logger.info("Handing something not supported")
|
||||
Logger.info("Handing something with type #{object["type"]} not supported")
|
||||
Logger.debug(inspect(object))
|
||||
{:error, :not_supported}
|
||||
end
|
||||
@@ -657,6 +768,52 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
|
||||
end
|
||||
end
|
||||
|
||||
# If the object has been announced by a group let's use one of our members to fetch it
|
||||
@spec fetch_object_optionnally_authenticated(String.t(), Actor.t() | any()) ::
|
||||
{:ok, struct()} | {:error, any()}
|
||||
defp fetch_object_optionnally_authenticated(url, %Actor{type: :Group, id: group_id}) do
|
||||
case Actors.get_single_group_member_actor(group_id) do
|
||||
%Actor{} = actor ->
|
||||
ActivityPub.fetch_object_from_url(url, on_behalf_of: actor, force: true)
|
||||
|
||||
_err ->
|
||||
fetch_object_optionnally_authenticated(url, nil)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_object_optionnally_authenticated(url, _),
|
||||
do: ActivityPub.fetch_object_from_url(url, force: true)
|
||||
|
||||
defp eventually_create_share(object, entity, actor_id) do
|
||||
with object_id <- object |> Utils.get_url(),
|
||||
%Actor{id: object_owner_actor_id} <- Ownable.actor(entity) do
|
||||
{:ok, %Mobilizon.Share{} = _share} =
|
||||
Mobilizon.Share.create(object_id, actor_id, object_owner_actor_id)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@spec is_data_for_comment_or_discussion?(map()) :: boolean()
|
||||
defp is_data_for_comment_or_discussion?(object_data) do
|
||||
(not Map.has_key?(object_data, :title) or
|
||||
is_nil(object_data.title) or object_data.title == "") and
|
||||
is_nil(object_data.discussion_id)
|
||||
end
|
||||
|
||||
# Comment and conversations have different attributes for actor and groups
|
||||
defp transform_object_data_for_discussion(object_data) do
|
||||
# Basic comment
|
||||
if is_data_for_comment_or_discussion?(object_data) do
|
||||
object_data
|
||||
else
|
||||
# Conversation
|
||||
object_data
|
||||
|> Map.put(:creator_id, object_data.actor_id)
|
||||
|> Map.put(:actor_id, object_data.attributed_to_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_follow(follow_object) do
|
||||
with follow_object_id when not is_nil(follow_object_id) <- Utils.get_url(follow_object),
|
||||
{:not_found, %Follower{} = follow} <-
|
||||
|
||||
74
lib/federation/activity_pub/types/actors.ex
Normal file
74
lib/federation/activity_pub/types/actors.ex
Normal file
@@ -0,0 +1,74 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
|
||||
@moduledoc false
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Audience
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
alias Mobilizon.Service.Formatter.HTML
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
|
||||
|
||||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, map()}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args_for_actor(args),
|
||||
{:ok, %Actor{} = actor} <- Actors.create_actor(args),
|
||||
actor_as_data <- Convertible.model_to_as(actor),
|
||||
audience <- %{"to" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(actor_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, actor, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Actor.t(), map, map) :: {:ok, Actor.t(), Activity.t()} | any
|
||||
def update(%Actor{} = old_actor, args, additional) do
|
||||
with {:ok, %Actor{} = new_actor} <- Actors.update_actor(old_actor, args),
|
||||
actor_as_data <- Convertible.model_to_as(new_actor),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "actor_#{new_actor.preferred_username}"),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(new_actor),
|
||||
additional <- Map.merge(additional, %{"actor" => old_actor.url}),
|
||||
update_data <- make_update_data(actor_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_actor, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
def delete(
|
||||
%Actor{followers_url: followers_url, url: target_actor_url} = target_actor,
|
||||
%Actor{url: actor_url} = actor,
|
||||
local
|
||||
) do
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor_url,
|
||||
"object" => Convertible.model_to_as(target_actor),
|
||||
"id" => target_actor_url <> "/delete",
|
||||
"to" => [followers_url, "https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
# We completely delete the actor if activity is remote
|
||||
with {:ok, %Oban.Job{}} <- Actors.delete_actor(target_actor, reserve_username: local) do
|
||||
{:ok, activity_data, actor, target_actor}
|
||||
end
|
||||
end
|
||||
|
||||
def actor(%Actor{} = actor), do: actor
|
||||
|
||||
def group_actor(%Actor{} = _actor), do: nil
|
||||
|
||||
defp prepare_args_for_actor(args) do
|
||||
with preferred_username <-
|
||||
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
|
||||
summary <- args |> Map.get(:summary, "") |> String.trim(),
|
||||
{summary, _mentions, _tags} <-
|
||||
summary |> String.trim() |> APIUtils.make_content_html([], "text/html") do
|
||||
%{args | preferred_username: preferred_username, summary: summary}
|
||||
end
|
||||
end
|
||||
end
|
||||
149
lib/federation/activity_pub/types/comments.ex
Normal file
149
lib/federation/activity_pub/types/comments.ex
Normal file
@@ -0,0 +1,149 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
|
||||
@moduledoc false
|
||||
alias Mobilizon.{Actors, Discussions, Events}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Federation.ActivityPub.Audience
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
alias Mobilizon.Share
|
||||
alias Mobilizon.Tombstone
|
||||
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, map()}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args_for_comment(args),
|
||||
{:ok, %Comment{discussion_id: discussion_id} = comment} <-
|
||||
Discussions.create_comment(args),
|
||||
:ok <- maybe_publish_graphql_subscription(discussion_id),
|
||||
comment_as_data <- Convertible.model_to_as(comment),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(comment),
|
||||
create_data <-
|
||||
make_create_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, comment, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Comment.t(), map(), map()) :: {:ok, Comment.t(), Activity.t()} | any()
|
||||
def update(%Comment{} = old_comment, args, additional) do
|
||||
with args <- prepare_args_for_comment(args),
|
||||
{:ok, %Comment{} = new_comment} <- Discussions.update_comment(old_comment, args),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{new_comment.uuid}"),
|
||||
comment_as_data <- Convertible.model_to_as(new_comment),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(new_comment),
|
||||
update_data <- make_update_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_comment, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Comment.t(), Actor.t(), boolean) :: {:ok, Comment.t()}
|
||||
def delete(%Comment{url: url} = comment, %Actor{} = actor, _local) do
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
"object" => Convertible.model_to_as(comment),
|
||||
"id" => url <> "/delete",
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"]
|
||||
}
|
||||
|
||||
with audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(comment),
|
||||
{:ok, %Comment{} = comment} <- Discussions.delete_comment(comment),
|
||||
# Preload to be sure
|
||||
%Comment{} = comment <- Discussions.get_comment_with_preload(comment.id),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "comment_#{comment.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: comment.url, actor_id: actor.id}) do
|
||||
Share.delete_all_by_uri(comment.url)
|
||||
{:ok, Map.merge(activity_data, audience), actor, comment}
|
||||
end
|
||||
end
|
||||
|
||||
def actor(%Comment{actor: %Actor{} = actor}), do: actor
|
||||
|
||||
def actor(%Comment{actor_id: actor_id}) when not is_nil(actor_id),
|
||||
do: Actors.get_actor(actor_id)
|
||||
|
||||
def actor(_), do: nil
|
||||
|
||||
def group_actor(%Comment{attributed_to: %Actor{} = group}), do: group
|
||||
|
||||
def group_actor(%Comment{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
|
||||
do: Actors.get_actor(attributed_to_id)
|
||||
|
||||
def group_actor(_), do: nil
|
||||
|
||||
# Prepare and sanitize arguments for comments
|
||||
defp prepare_args_for_comment(args) do
|
||||
with in_reply_to_comment <-
|
||||
args |> Map.get(:in_reply_to_comment_id) |> Discussions.get_comment_with_preload(),
|
||||
event <- args |> Map.get(:event_id) |> handle_event_for_comment(),
|
||||
args <- Map.update(args, :visibility, :public, & &1),
|
||||
{text, mentions, tags} <-
|
||||
APIUtils.make_content_html(
|
||||
args |> Map.get(:text, "") |> String.trim(),
|
||||
# Can't put additional tags on a comment
|
||||
[],
|
||||
"text/html"
|
||||
),
|
||||
tags <- ConverterUtils.fetch_tags(tags),
|
||||
mentions <- Map.get(args, :mentions, []) ++ ConverterUtils.fetch_mentions(mentions),
|
||||
args <-
|
||||
Map.merge(args, %{
|
||||
actor_id: Map.get(args, :actor_id),
|
||||
text: text,
|
||||
mentions: mentions,
|
||||
tags: tags,
|
||||
event: event,
|
||||
in_reply_to_comment: in_reply_to_comment,
|
||||
in_reply_to_comment_id:
|
||||
if(is_nil(in_reply_to_comment), do: nil, else: Map.get(in_reply_to_comment, :id)),
|
||||
origin_comment_id:
|
||||
if(is_nil(in_reply_to_comment),
|
||||
do: nil,
|
||||
else: Comment.get_thread_id(in_reply_to_comment)
|
||||
)
|
||||
}) do
|
||||
args
|
||||
end
|
||||
end
|
||||
|
||||
@spec handle_event_for_comment(String.t() | integer() | nil) :: Event.t() | nil
|
||||
defp handle_event_for_comment(event_id) when not is_nil(event_id) do
|
||||
case Events.get_event_with_preload(event_id) do
|
||||
{:ok, %Event{} = event} -> event
|
||||
{:error, :event_not_found} -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_event_for_comment(nil), do: nil
|
||||
|
||||
defp maybe_publish_graphql_subscription(nil), do: :ok
|
||||
|
||||
defp maybe_publish_graphql_subscription(discussion_id) do
|
||||
with %Discussion{} = discussion <- Discussions.get_discussion(discussion_id) do
|
||||
Absinthe.Subscription.publish(Endpoint, discussion,
|
||||
discussion_comment_changed: discussion.slug
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
115
lib/federation/activity_pub/types/discussions.ex
Normal file
115
lib/federation/activity_pub/types/discussions.ex
Normal file
@@ -0,0 +1,115 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
|
||||
@moduledoc false
|
||||
|
||||
alias Mobilizon.{Actors, Discussions}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Federation.ActivityPub.Audience
|
||||
alias Mobilizon.Federation.ActivityPub.Types.{Comments, Entity}
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Storage.Repo
|
||||
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, map()}
|
||||
def create(%{discussion_id: discussion_id} = args, additional) when not is_nil(discussion_id) do
|
||||
with %Discussion{} = discussion <- Discussions.get_discussion(discussion_id),
|
||||
{:ok, %Discussion{last_comment_id: last_comment_id} = discussion} <-
|
||||
Discussions.reply_to_discussion(discussion, args),
|
||||
%Comment{} = last_comment <- Discussions.get_comment_with_preload(last_comment_id),
|
||||
:ok <- maybe_publish_graphql_subscription(discussion),
|
||||
comment_as_data <- Convertible.model_to_as(last_comment),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(discussion),
|
||||
create_data <-
|
||||
make_create_data(comment_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, discussion, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec create(map(), map()) :: {:ok, map()}
|
||||
def create(args, additional) do
|
||||
with {:ok, %Discussion{} = discussion} <-
|
||||
Discussions.create_discussion(args),
|
||||
discussion_as_data <- Convertible.model_to_as(discussion),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(discussion),
|
||||
create_data <-
|
||||
make_create_data(discussion_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, discussion, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Discussion.t(), map(), map()) :: {:ok, Discussion.t(), Activity.t()} | any()
|
||||
def update(%Discussion{} = old_discussion, args, additional) do
|
||||
with {:ok, %Discussion{} = new_discussion} <-
|
||||
Discussions.update_discussion(old_discussion, args),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "discussion_#{new_discussion.slug}"),
|
||||
discussion_as_data <- Convertible.model_to_as(new_discussion),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(new_discussion),
|
||||
update_data <- make_update_data(discussion_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_discussion, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Discussion.t(), Actor.t(), boolean) :: {:ok, Discussion.t()}
|
||||
def delete(%Discussion{actor: group, url: url} = discussion, %Actor{} = actor, _local) do
|
||||
stream =
|
||||
discussion.comments
|
||||
|> Enum.map(
|
||||
&Repo.preload(&1, [
|
||||
:actor,
|
||||
:attributed_to,
|
||||
:in_reply_to_comment,
|
||||
:mentions,
|
||||
:origin_comment,
|
||||
:discussion,
|
||||
:tags,
|
||||
:replies
|
||||
])
|
||||
)
|
||||
|> Enum.map(&Map.put(&1, :event, nil))
|
||||
|> Task.async_stream(fn comment -> Comments.delete(comment, actor, nil) end)
|
||||
|
||||
Stream.run(stream)
|
||||
|
||||
with {:ok, %Discussion{}} <- Discussions.delete_discussion(discussion) do
|
||||
# This is just fake
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
"object" => Convertible.model_to_as(discussion),
|
||||
"id" => url <> "/delete",
|
||||
"to" => [group.members_url]
|
||||
}
|
||||
|
||||
{:ok, activity_data, actor, discussion}
|
||||
end
|
||||
end
|
||||
|
||||
def actor(%Discussion{creator_id: creator_id}), do: Actors.get_actor(creator_id)
|
||||
|
||||
def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id)
|
||||
|
||||
@spec maybe_publish_graphql_subscription(Discussion.t()) :: :ok
|
||||
defp maybe_publish_graphql_subscription(%Discussion{} = discussion) do
|
||||
Absinthe.Subscription.publish(Endpoint, discussion,
|
||||
discussion_comment_changed: discussion.slug
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
151
lib/federation/activity_pub/types/entity.ex
Normal file
151
lib/federation/activity_pub/types/entity.ex
Normal file
@@ -0,0 +1,151 @@
|
||||
alias Mobilizon.Federation.ActivityPub.Types.{
|
||||
Actors,
|
||||
Comments,
|
||||
Discussions,
|
||||
Entity,
|
||||
Events,
|
||||
Managable,
|
||||
Ownable,
|
||||
Posts,
|
||||
Resources,
|
||||
Todos,
|
||||
TodoLists,
|
||||
Tombstones
|
||||
}
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Todos.{Todo, TodoList}
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Tombstone
|
||||
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Entity do
|
||||
@moduledoc """
|
||||
ActivityPub entity behaviour
|
||||
"""
|
||||
@type t :: %{id: String.t()}
|
||||
|
||||
@callback create(data :: any(), additionnal :: map()) ::
|
||||
{:ok, t(), ActivityStream.t()}
|
||||
|
||||
@callback update(struct :: t(), attrs :: map(), additionnal :: map()) ::
|
||||
{:ok, t(), ActivityStream.t()}
|
||||
|
||||
@callback delete(struct :: t(), Actor.t(), local :: boolean()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), t()}
|
||||
end
|
||||
|
||||
defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
|
||||
@moduledoc """
|
||||
ActivityPub entity Managable protocol.
|
||||
"""
|
||||
|
||||
@spec update(Entity.t(), map(), map()) :: {:ok, Entity.t(), ActivityStream.t()}
|
||||
@doc """
|
||||
Updates a `Managable` entity with the appropriate attributes and returns the updated entity and an activitystream representation for it
|
||||
"""
|
||||
def update(entity, attrs, additionnal)
|
||||
|
||||
@spec delete(Entity.t(), Actor.t(), boolean()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), Entity.t()}
|
||||
@doc "Deletes an entity and returns the activitystream representation for it"
|
||||
def delete(entity, actor, local)
|
||||
end
|
||||
|
||||
defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
|
||||
@spec group_actor(Entity.t()) :: Actor.t() | nil
|
||||
@doc "Returns an eventual group for the entity"
|
||||
def group_actor(entity)
|
||||
|
||||
@spec actor(Entity.t()) :: Actor.t() | nil
|
||||
@doc "Returns the actor for the entity"
|
||||
def actor(entity)
|
||||
end
|
||||
|
||||
defimpl Managable, for: Event do
|
||||
defdelegate update(entity, attrs, additionnal), to: Events
|
||||
defdelegate delete(entity, actor, local), to: Events
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Event do
|
||||
defdelegate group_actor(entity), to: Events
|
||||
defdelegate actor(entity), to: Events
|
||||
end
|
||||
|
||||
defimpl Managable, for: Comment do
|
||||
defdelegate update(entity, attrs, additionnal), to: Comments
|
||||
defdelegate delete(entity, actor, local), to: Comments
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Comment do
|
||||
defdelegate group_actor(entity), to: Comments
|
||||
defdelegate actor(entity), to: Comments
|
||||
end
|
||||
|
||||
defimpl Managable, for: Post do
|
||||
defdelegate update(entity, attrs, additionnal), to: Posts
|
||||
defdelegate delete(entity, actor, local), to: Posts
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Post do
|
||||
defdelegate group_actor(entity), to: Posts
|
||||
defdelegate actor(entity), to: Posts
|
||||
end
|
||||
|
||||
defimpl Managable, for: Actor do
|
||||
defdelegate update(entity, attrs, additionnal), to: Actors
|
||||
defdelegate delete(entity, actor, local), to: Actors
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Actor do
|
||||
defdelegate group_actor(entity), to: Actors
|
||||
defdelegate actor(entity), to: Actors
|
||||
end
|
||||
|
||||
defimpl Managable, for: TodoList do
|
||||
defdelegate update(entity, attrs, additionnal), to: TodoLists
|
||||
defdelegate delete(entity, actor, local), to: TodoLists
|
||||
end
|
||||
|
||||
defimpl Ownable, for: TodoList do
|
||||
defdelegate group_actor(entity), to: TodoLists
|
||||
defdelegate actor(entity), to: TodoLists
|
||||
end
|
||||
|
||||
defimpl Managable, for: Todo do
|
||||
defdelegate update(entity, attrs, additionnal), to: Todos
|
||||
defdelegate delete(entity, actor, local), to: Todos
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Todo do
|
||||
defdelegate group_actor(entity), to: Todos
|
||||
defdelegate actor(entity), to: Todos
|
||||
end
|
||||
|
||||
defimpl Managable, for: Resource do
|
||||
defdelegate update(entity, attrs, additionnal), to: Resources
|
||||
defdelegate delete(entity, actor, local), to: Resources
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Resource do
|
||||
defdelegate group_actor(entity), to: Resources
|
||||
defdelegate actor(entity), to: Resources
|
||||
end
|
||||
|
||||
defimpl Managable, for: Discussion do
|
||||
defdelegate update(entity, attrs, additionnal), to: Discussions
|
||||
defdelegate delete(entity, actor, local), to: Discussions
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Discussion do
|
||||
defdelegate group_actor(entity), to: Discussions
|
||||
defdelegate actor(entity), to: Discussions
|
||||
end
|
||||
|
||||
defimpl Ownable, for: Tombstone do
|
||||
defdelegate group_actor(entity), to: Tombstones
|
||||
defdelegate actor(entity), to: Tombstones
|
||||
end
|
||||
203
lib/federation/activity_pub/types/events.ex
Normal file
203
lib/federation/activity_pub/types/events.ex
Normal file
@@ -0,0 +1,203 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||
@moduledoc false
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events, as: EventsManager
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Audience
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.GraphQL.API.Utils, as: APIUtils
|
||||
alias Mobilizon.Service.Formatter.HTML
|
||||
alias Mobilizon.Service.Notifications.Scheduler
|
||||
alias Mobilizon.Share
|
||||
alias Mobilizon.Tombstone
|
||||
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, map()}
|
||||
def create(args, additional) do
|
||||
with args <- prepare_args_for_event(args),
|
||||
{:ok, %Event{} = event} <- EventsManager.create_event(args),
|
||||
event_as_data <- Convertible.model_to_as(event),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(event),
|
||||
create_data <-
|
||||
make_create_data(event_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, event, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Event.t(), map(), map()) :: {:ok, Event.t(), Activity.t()} | any()
|
||||
def update(%Event{} = old_event, args, additional) do
|
||||
with args <- prepare_args_for_event(args),
|
||||
{:ok, %Event{} = new_event} <- EventsManager.update_event(old_event, args),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "event_#{new_event.uuid}"),
|
||||
event_as_data <- Convertible.model_to_as(new_event),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(new_event),
|
||||
update_data <- make_update_data(event_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, new_event, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.error("Something went wrong while creating an update activity")
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Event.t(), Actor.t(), boolean) :: {:ok, Event.t()}
|
||||
def delete(%Event{url: url} = event, %Actor{} = actor, _local) do
|
||||
activity_data = %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor.url,
|
||||
"object" => Convertible.model_to_as(event),
|
||||
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"],
|
||||
"id" => url <> "/delete"
|
||||
}
|
||||
|
||||
with audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(event),
|
||||
{:ok, %Event{} = event} <- EventsManager.delete_event(event),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "event_#{event.uuid}"),
|
||||
{:ok, %Tombstone{} = _tombstone} <-
|
||||
Tombstone.create_tombstone(%{uri: event.url, actor_id: actor.id}) do
|
||||
Share.delete_all_by_uri(event.url)
|
||||
{:ok, Map.merge(activity_data, audience), actor, event}
|
||||
end
|
||||
end
|
||||
|
||||
def actor(%Event{organizer_actor: %Actor{} = actor}), do: actor
|
||||
|
||||
def actor(%Event{organizer_actor_id: organizer_actor_id}),
|
||||
do: Actors.get_actor(organizer_actor_id)
|
||||
|
||||
def actor(_), do: nil
|
||||
|
||||
def group_actor(%Event{attributed_to: %Actor{} = group}), do: group
|
||||
|
||||
def group_actor(%Event{attributed_to_id: attributed_to_id}) when not is_nil(attributed_to_id),
|
||||
do: Actors.get_actor(attributed_to_id)
|
||||
|
||||
def group_actor(_), do: nil
|
||||
|
||||
def join(%Event{} = event, %Actor{} = actor, _local, additional) do
|
||||
with {:maximum_attendee_capacity, true} <-
|
||||
{:maximum_attendee_capacity, check_attendee_capacity(event)},
|
||||
role <-
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.get(:role, Mobilizon.Events.get_default_participant_role(event)),
|
||||
{:ok, %Participant{} = participant} <-
|
||||
Mobilizon.Events.create_participant(%{
|
||||
role: role,
|
||||
event_id: event.id,
|
||||
actor_id: actor.id,
|
||||
url: Map.get(additional, :url),
|
||||
metadata:
|
||||
additional
|
||||
|> Map.get(:metadata, %{})
|
||||
|> Map.update(:message, nil, &String.trim(HTML.strip_tags(&1)))
|
||||
}),
|
||||
join_data <- Convertible.model_to_as(participant),
|
||||
audience <-
|
||||
Audience.calculate_to_and_cc_from_mentions(participant) do
|
||||
approve_if_default_role_is_participant(
|
||||
event,
|
||||
Map.merge(join_data, audience),
|
||||
participant,
|
||||
role
|
||||
)
|
||||
else
|
||||
{:maximum_attendee_capacity, err} ->
|
||||
{:maximum_attendee_capacity, err}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_attendee_capacity(%Event{options: options} = event) do
|
||||
with maximum_attendee_capacity <-
|
||||
Map.get(options, :maximum_attendee_capacity) || 0 do
|
||||
maximum_attendee_capacity == 0 ||
|
||||
Mobilizon.Events.count_participant_participants(event.id) < maximum_attendee_capacity
|
||||
end
|
||||
end
|
||||
|
||||
# Set the participant to approved if the default role for new participants is :participant
|
||||
defp approve_if_default_role_is_participant(event, activity_data, participant, role) do
|
||||
if event.local do
|
||||
cond do
|
||||
Mobilizon.Events.get_default_participant_role(event) === :participant &&
|
||||
role == :participant ->
|
||||
{:accept,
|
||||
ActivityPub.accept(
|
||||
:join,
|
||||
participant,
|
||||
true,
|
||||
%{"actor" => event.organizer_actor.url}
|
||||
)}
|
||||
|
||||
Mobilizon.Events.get_default_participant_role(event) === :not_approved &&
|
||||
role == :not_approved ->
|
||||
Scheduler.pending_participation_notification(event)
|
||||
{:ok, activity_data, participant}
|
||||
|
||||
true ->
|
||||
{:ok, activity_data, participant}
|
||||
end
|
||||
else
|
||||
{:ok, activity_data, participant}
|
||||
end
|
||||
end
|
||||
|
||||
# Prepare and sanitize arguments for events
|
||||
defp prepare_args_for_event(args) do
|
||||
# If title is not set: we are not updating it
|
||||
args =
|
||||
if Map.has_key?(args, :title) && !is_nil(args.title),
|
||||
do: Map.update(args, :title, "", &String.trim/1),
|
||||
else: args
|
||||
|
||||
# If we've been given a description (we might not get one if updating)
|
||||
# sanitize it, HTML it, and extract tags & mentions from it
|
||||
args =
|
||||
if Map.has_key?(args, :description) && !is_nil(args.description) do
|
||||
{description, mentions, tags} =
|
||||
APIUtils.make_content_html(
|
||||
String.trim(args.description),
|
||||
Map.get(args, :tags, []),
|
||||
"text/html"
|
||||
)
|
||||
|
||||
mentions = ConverterUtils.fetch_mentions(Map.get(args, :mentions, []) ++ mentions)
|
||||
|
||||
Map.merge(args, %{
|
||||
description: description,
|
||||
mentions: mentions,
|
||||
tags: tags
|
||||
})
|
||||
else
|
||||
args
|
||||
end
|
||||
|
||||
# Check that we can only allow anonymous participation if our instance allows it
|
||||
{_, options} =
|
||||
Map.get_and_update(
|
||||
Map.get(args, :options, %{anonymous_participation: false}),
|
||||
:anonymous_participation,
|
||||
fn value ->
|
||||
{value, value && Mobilizon.Config.anonymous_participation?()}
|
||||
end
|
||||
)
|
||||
|
||||
args = Map.put(args, :options, options)
|
||||
|
||||
Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1)
|
||||
end
|
||||
end
|
||||
93
lib/federation/activity_pub/types/posts.ex
Normal file
93
lib/federation/activity_pub/types/posts.ex
Normal file
@@ -0,0 +1,93 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
|
||||
@moduledoc false
|
||||
alias Mobilizon.{Actors, Posts}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Posts.Post
|
||||
require Logger
|
||||
import Mobilizon.Federation.ActivityPub.Utils, only: [make_create_data: 2, make_update_data: 2]
|
||||
|
||||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
def create(args, additional) do
|
||||
with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1),
|
||||
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
|
||||
Posts.create_post(args),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
|
||||
post_as_data <-
|
||||
Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
|
||||
audience <- %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
} do
|
||||
create_data = make_create_data(post_as_data, Map.merge(audience, additional))
|
||||
|
||||
{:ok, post, create_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
def update(%Post{} = post, args, additional) do
|
||||
with args <- Map.update(args, :tags, [], &ConverterUtils.fetch_tags/1),
|
||||
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
|
||||
Posts.update_post(post, args),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}"),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
|
||||
post_as_data <-
|
||||
Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
|
||||
audience <- %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
} do
|
||||
update_data = make_update_data(post_as_data, Map.merge(audience, additional))
|
||||
|
||||
{:ok, post, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
def delete(
|
||||
%Post{
|
||||
url: url,
|
||||
attributed_to: %Actor{url: group_url}
|
||||
} = post,
|
||||
%Actor{url: actor_url} = actor,
|
||||
_local
|
||||
) do
|
||||
activity_data = %{
|
||||
"actor" => actor_url,
|
||||
"type" => "Delete",
|
||||
"object" => Convertible.model_to_as(post),
|
||||
"id" => url <> "/delete",
|
||||
"to" => [group_url]
|
||||
}
|
||||
|
||||
with {:ok, _post} <- Posts.delete_post(post),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}") do
|
||||
{:ok, activity_data, actor, post}
|
||||
end
|
||||
end
|
||||
|
||||
def actor(%Post{author_id: author_id}),
|
||||
do: Actors.get_actor(author_id)
|
||||
|
||||
def group_actor(%Post{attributed_to_id: attributed_to_id}),
|
||||
do: Actors.get_actor(attributed_to_id)
|
||||
end
|
||||
43
lib/federation/activity_pub/types/reports.ex
Normal file
43
lib/federation/activity_pub/types/reports.ex
Normal file
@@ -0,0 +1,43 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Reports do
|
||||
@moduledoc false
|
||||
alias Mobilizon.{Actors, Discussions, Reports}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Reports.Report
|
||||
alias Mobilizon.Service.Formatter.HTML
|
||||
require Logger
|
||||
|
||||
def flag(args, local \\ false, _additional \\ %{}) do
|
||||
with {:build_args, args} <- {:build_args, prepare_args_for_report(args)},
|
||||
{:create_report, {:ok, %Report{} = report}} <-
|
||||
{:create_report, Reports.create_report(args)},
|
||||
report_as_data <- Convertible.model_to_as(report),
|
||||
cc <- if(local, do: [report.reported.url], else: []),
|
||||
report_as_data <- Map.merge(report_as_data, %{"to" => [], "cc" => cc}) do
|
||||
{report, report_as_data}
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_args_for_report(args) do
|
||||
with {:reporter, %Actor{} = reporter_actor} <-
|
||||
{:reporter, Actors.get_actor!(args.reporter_id)},
|
||||
{:reported, %Actor{} = reported_actor} <-
|
||||
{:reported, Actors.get_actor!(args.reported_id)},
|
||||
content <- HTML.strip_tags(args.content),
|
||||
event <- Discussions.get_comment(Map.get(args, :event_id)),
|
||||
{:get_report_comments, comments} <-
|
||||
{:get_report_comments,
|
||||
Discussions.list_comments_by_actor_and_ids(
|
||||
reported_actor.id,
|
||||
Map.get(args, :comments_ids, [])
|
||||
)} do
|
||||
Map.merge(args, %{
|
||||
reporter: reporter_actor,
|
||||
reported: reported_actor,
|
||||
content: content,
|
||||
event: event,
|
||||
comments: comments
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
157
lib/federation/activity_pub/types/resources.ex
Normal file
157
lib/federation/activity_pub/types/resources.ex
Normal file
@@ -0,0 +1,157 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
|
||||
@moduledoc false
|
||||
alias Mobilizon.{Actors, Resources}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Service.RichMedia.Parser
|
||||
require Logger
|
||||
|
||||
import Mobilizon.Federation.ActivityPub.Utils,
|
||||
only: [make_create_data: 2, make_update_data: 2, make_add_data: 3, make_move_data: 4]
|
||||
|
||||
@behaviour Entity
|
||||
|
||||
@impl Entity
|
||||
def create(%{type: type} = args, additional) do
|
||||
args =
|
||||
case type do
|
||||
:folder ->
|
||||
args
|
||||
|
||||
_ ->
|
||||
case Parser.parse(Map.get(args, :resource_url)) do
|
||||
{:ok, metadata} ->
|
||||
Map.put(args, :metadata, metadata)
|
||||
|
||||
_ ->
|
||||
args
|
||||
end
|
||||
end
|
||||
|
||||
with {:ok,
|
||||
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: parent_id} = resource} <-
|
||||
Resources.create_resource(args),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group, creator: creator}),
|
||||
audience <- %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
} do
|
||||
create_data =
|
||||
case parent_id do
|
||||
nil ->
|
||||
make_create_data(resource_as_data, Map.merge(audience, additional))
|
||||
|
||||
parent_id ->
|
||||
# In case the resource has a parent we don't `Create` the resource but `Add` it to an existing resource
|
||||
parent = Resources.get_resource(parent_id)
|
||||
make_add_data(resource_as_data, parent, Map.merge(audience, additional))
|
||||
end
|
||||
|
||||
{:ok, resource, create_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
def update(%Resource{} = old_resource, %{parent_id: _parent_id} = args, additional) do
|
||||
move(old_resource, args, additional)
|
||||
end
|
||||
|
||||
# Simple rename
|
||||
def update(%Resource{} = old_resource, %{title: title} = _args, additional) do
|
||||
with {:ok, %Resource{actor_id: group_id, creator_id: creator_id} = resource} <-
|
||||
Resources.update_resource(old_resource, %{title: title}),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group}),
|
||||
audience <- %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
},
|
||||
update_data <-
|
||||
make_update_data(resource_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, resource, update_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
def move(
|
||||
%Resource{parent_id: old_parent_id} = old_resource,
|
||||
%{parent_id: _new_parent_id} = args,
|
||||
additional
|
||||
) do
|
||||
with {:ok,
|
||||
%Resource{actor_id: group_id, creator_id: creator_id, parent_id: new_parent_id} =
|
||||
resource} <-
|
||||
Resources.update_resource(old_resource, args),
|
||||
old_parent <- Resources.get_resource(old_parent_id),
|
||||
new_parent <- Resources.get_resource(new_parent_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
%Actor{url: creator_url} <- Actors.get_actor(creator_id),
|
||||
resource_as_data <-
|
||||
Convertible.model_to_as(%{resource | actor: group}),
|
||||
audience <- %{
|
||||
"to" => [group.members_url],
|
||||
"cc" => [],
|
||||
"actor" => creator_url,
|
||||
"attributedTo" => [creator_url]
|
||||
},
|
||||
move_data <-
|
||||
make_move_data(
|
||||
resource_as_data,
|
||||
old_parent,
|
||||
new_parent,
|
||||
Map.merge(audience, additional)
|
||||
) do
|
||||
{:ok, resource, move_data}
|
||||
else
|
||||
err ->
|
||||
Logger.debug(inspect(err))
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
def delete(
|
||||
%Resource{url: url, actor: %Actor{url: group_url, members_url: members_url}} = resource,
|
||||
%Actor{url: actor_url} = actor,
|
||||
_local
|
||||
) do
|
||||
Logger.debug("Building Delete Resource activity")
|
||||
|
||||
activity_data = %{
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => [group_url],
|
||||
"type" => "Delete",
|
||||
"object" => Convertible.model_to_as(resource),
|
||||
"id" => url <> "/delete",
|
||||
"to" => [members_url]
|
||||
}
|
||||
|
||||
with {:ok, _resource} <- Resources.delete_resource(resource),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "resource_#{resource.id}") do
|
||||
{:ok, activity_data, actor, resource}
|
||||
end
|
||||
end
|
||||
|
||||
def actor(%Resource{creator_id: creator_id}),
|
||||
do: Actors.get_actor(creator_id)
|
||||
|
||||
def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id)
|
||||
end
|
||||
69
lib/federation/activity_pub/types/todo_lists.ex
Normal file
69
lib/federation/activity_pub/types/todo_lists.ex
Normal file
@@ -0,0 +1,69 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
|
||||
@moduledoc false
|
||||
alias Mobilizon.{Actors, Todos}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Todos.TodoList
|
||||
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, map()}
|
||||
def create(args, additional) do
|
||||
with {:ok, %TodoList{actor_id: group_id} = todo_list} <- Todos.create_todo_list(args),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_list_as_data <- Convertible.model_to_as(%{todo_list | actor: group}),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(todo_list_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, todo_list, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(TodoList.t(), map, map) :: {:ok, TodoList.t(), Activity.t()} | any
|
||||
def update(%TodoList{} = old_todo_list, args, additional) do
|
||||
with {:ok, %TodoList{actor_id: group_id} = todo_list} <-
|
||||
Todos.update_todo_list(old_todo_list, args),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_list_as_data <-
|
||||
Convertible.model_to_as(%{todo_list | actor: group}),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
update_data <-
|
||||
make_update_data(todo_list_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, todo_list, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(TodoList.t(), Actor.t(), boolean()) ::
|
||||
{:ok, ActivityStream.t(), Actor.t(), TodoList.t()}
|
||||
def delete(
|
||||
%TodoList{url: url, actor: %Actor{url: group_url}} = todo_list,
|
||||
%Actor{url: actor_url} = actor,
|
||||
_local
|
||||
) do
|
||||
Logger.debug("Building Delete TodoList activity")
|
||||
|
||||
activity_data = %{
|
||||
"actor" => actor_url,
|
||||
"type" => "Delete",
|
||||
"object" => Convertible.model_to_as(url),
|
||||
"id" => url <> "/delete",
|
||||
"to" => [group_url]
|
||||
}
|
||||
|
||||
with {:ok, _todo_list} <- Todos.delete_todo_list(todo_list),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "todo_list_#{todo_list.id}") do
|
||||
{:ok, activity_data, actor, todo_list}
|
||||
end
|
||||
end
|
||||
|
||||
def actor(%TodoList{}), do: nil
|
||||
|
||||
def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id)
|
||||
end
|
||||
80
lib/federation/activity_pub/types/todos.ex
Normal file
80
lib/federation/activity_pub/types/todos.ex
Normal file
@@ -0,0 +1,80 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
|
||||
@moduledoc false
|
||||
alias Mobilizon.{Actors, Todos}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Entity
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
alias Mobilizon.Todos.{Todo, TodoList}
|
||||
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, map()}
|
||||
def create(args, additional) do
|
||||
with {:ok, %Todo{todo_list_id: todo_list_id, creator_id: creator_id} = todo} <-
|
||||
Todos.create_todo(args),
|
||||
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
|
||||
%Actor{} = creator <- Actors.get_actor(creator_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo <- %{todo | todo_list: %{todo_list | actor: group}, creator: creator},
|
||||
todo_as_data <-
|
||||
Convertible.model_to_as(todo),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
create_data <-
|
||||
make_create_data(todo_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, todo, create_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec update(Todo.t(), map, map) :: {:ok, Todo.t(), Activity.t()} | any
|
||||
def update(%Todo{} = old_todo, args, additional) do
|
||||
with {:ok, %Todo{todo_list_id: todo_list_id} = todo} <- Todos.update_todo(old_todo, args),
|
||||
%TodoList{actor_id: group_id} = todo_list <- Todos.get_todo_list(todo_list_id),
|
||||
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
|
||||
todo_as_data <-
|
||||
Convertible.model_to_as(%{todo | todo_list: %{todo_list | actor: group}}),
|
||||
audience <- %{"to" => [group.members_url], "cc" => []},
|
||||
update_data <-
|
||||
make_update_data(todo_as_data, Map.merge(audience, additional)) do
|
||||
{:ok, todo, update_data}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Entity
|
||||
@spec delete(Todo.t(), Actor.t(), boolean()) :: {:ok, ActivityStream.t(), Actor.t(), Todo.t()}
|
||||
def delete(
|
||||
%Todo{url: url, creator: %Actor{url: group_url}} = todo,
|
||||
%Actor{url: actor_url} = actor,
|
||||
_local
|
||||
) do
|
||||
Logger.debug("Building Delete Todo activity")
|
||||
|
||||
activity_data = %{
|
||||
"actor" => actor_url,
|
||||
"type" => "Delete",
|
||||
"object" => Convertible.model_to_as(url),
|
||||
"id" => url <> "/delete",
|
||||
"to" => [group_url]
|
||||
}
|
||||
|
||||
with {:ok, _todo} <- Todos.delete_todo(todo),
|
||||
{:ok, true} <- Cachex.del(:activity_pub, "todo_#{todo.id}") do
|
||||
{:ok, activity_data, actor, todo}
|
||||
end
|
||||
end
|
||||
|
||||
def actor(%Todo{creator_id: creator_id}), do: Actors.get_actor(creator_id)
|
||||
|
||||
def group_actor(%Todo{todo_list_id: todo_list_id}) do
|
||||
case Todos.get_todo_list(todo_list_id) do
|
||||
%TodoList{actor_id: group_id} ->
|
||||
Actors.get_actor(group_id)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
14
lib/federation/activity_pub/types/tombstones.ex
Normal file
14
lib/federation/activity_pub/types/tombstones.ex
Normal file
@@ -0,0 +1,14 @@
|
||||
defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
|
||||
@moduledoc false
|
||||
alias Mobilizon.{Actors, Tombstone}
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
def actor(%Tombstone{actor: %Actor{id: actor_id}}), do: Actors.get_actor(actor_id)
|
||||
|
||||
def actor(%Tombstone{actor_id: actor_id}) when not is_nil(actor_id),
|
||||
do: Actors.get_actor(actor_id)
|
||||
|
||||
def actor(_), do: nil
|
||||
|
||||
def group_actor(_), do: nil
|
||||
end
|
||||
@@ -8,13 +8,16 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
Various ActivityPub related utils.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media.Picture
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay}
|
||||
alias Mobilizon.Federation.ActivityPub.Types.Ownable
|
||||
alias Mobilizon.Federation.ActivityStream.Converter
|
||||
alias Mobilizon.Federation.HTTPSignatures
|
||||
alias Mobilizon.Web.Endpoint
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -114,6 +117,53 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
|
||||
def maybe_federate(_), do: :ok
|
||||
|
||||
@doc """
|
||||
Applies to activities sent by group members from outside this instance to a group of this instance,
|
||||
we then need to relay (`Announce`) the object to other members on other instances.
|
||||
"""
|
||||
def maybe_relay_if_group_activity(activity, attributed_to \\ nil)
|
||||
|
||||
def maybe_relay_if_group_activity(
|
||||
%Activity{local: false, data: %{"object" => object}},
|
||||
_attributed_to
|
||||
)
|
||||
when is_map(object) do
|
||||
do_maybe_relay_if_group_activity(object, object["attributedTo"])
|
||||
end
|
||||
|
||||
# When doing a delete the object is just an AP ID string, so we pass the attributed_to actor as well
|
||||
def maybe_relay_if_group_activity(
|
||||
%Activity{local: false, data: %{"object" => object}},
|
||||
%Actor{url: attributed_to_url}
|
||||
)
|
||||
when is_binary(object) do
|
||||
do_maybe_relay_if_group_activity(object, attributed_to_url)
|
||||
end
|
||||
|
||||
def maybe_relay_if_group_activity(_, _), do: :ok
|
||||
|
||||
defp do_maybe_relay_if_group_activity(object, attributed_to) when not is_nil(attributed_to) do
|
||||
id = "#{Endpoint.url()}/announces/#{Ecto.UUID.generate()}"
|
||||
|
||||
case Actors.get_local_group_by_url(attributed_to) do
|
||||
%Actor{} = group ->
|
||||
case ActivityPub.announce(group, object, id, true, false) do
|
||||
{:ok, _activity, _object} ->
|
||||
Logger.info("Forwarded activity to external members of the group")
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
Logger.info("Failed to forward activity to external members of the group")
|
||||
:error
|
||||
end
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp do_maybe_relay_if_group_activity(_, _), do: :ok
|
||||
|
||||
@spec remote_actors(list(String.t())) :: list(Actor.t())
|
||||
def remote_actors(recipients) do
|
||||
recipients
|
||||
@@ -135,7 +185,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
Adds an id and a published data if they aren't there,
|
||||
also adds it to an included object
|
||||
"""
|
||||
def lazy_put_activity_defaults(map) do
|
||||
def lazy_put_activity_defaults(%{"object" => _object} = map) do
|
||||
if is_map(map["object"]) do
|
||||
object = lazy_put_object_defaults(map["object"])
|
||||
%{map | "object" => object}
|
||||
@@ -147,7 +197,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
@doc """
|
||||
Adds an id and published date if they aren't there.
|
||||
"""
|
||||
def lazy_put_object_defaults(map) do
|
||||
def lazy_put_object_defaults(map) when is_map(map) do
|
||||
Map.put_new_lazy(map, "published", &make_date/0)
|
||||
end
|
||||
|
||||
@@ -175,25 +225,49 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
|
||||
@doc """
|
||||
Checks that an incoming AP object's actor matches the domain it came from.
|
||||
|
||||
Takes the actor or attributedTo attributes (considers only the first elem if they're an array)
|
||||
"""
|
||||
def origin_check?(id, %{"actor" => actor, "attributedTo" => _attributed_to} = params)
|
||||
when not is_nil(actor) and actor != "" do
|
||||
params = Map.delete(params, "attributedTo")
|
||||
origin_check?(id, params)
|
||||
end
|
||||
|
||||
def origin_check?(id, %{"attributedTo" => actor} = params) do
|
||||
params = params |> Map.put("actor", actor) |> Map.delete("attributedTo")
|
||||
origin_check?(id, params)
|
||||
end
|
||||
|
||||
def origin_check?(id, %{"actor" => actor} = params) when not is_nil(actor) do
|
||||
id_uri = URI.parse(id)
|
||||
actor_uri = URI.parse(get_actor(params))
|
||||
|
||||
compare_uris?(actor_uri, id_uri)
|
||||
def origin_check?(id, %{"actor" => actor} = params)
|
||||
when not is_nil(actor) and is_list(actor) and length(actor) > 0 do
|
||||
origin_check?(id, Map.put(params, "actor", hd(actor)))
|
||||
end
|
||||
|
||||
def origin_check?(_id, %{"actor" => nil}), do: false
|
||||
def origin_check?(id, %{"actor" => actor} = params)
|
||||
when not is_nil(actor) do
|
||||
actor = get_actor(params)
|
||||
Logger.debug("Performing origin check on #{id} and #{actor} URIs")
|
||||
compare_origins?(id, actor)
|
||||
end
|
||||
|
||||
def origin_check?(_id, _data), do: false
|
||||
def origin_check?(_id, %{"type" => type} = _params) when type in ["Actor", "Group"], do: true
|
||||
|
||||
def origin_check?(_id, %{"actor" => nil} = _args), do: false
|
||||
|
||||
def origin_check?(_id, _args), do: false
|
||||
|
||||
@spec compare_origins?(String.t(), String.t()) :: boolean()
|
||||
def compare_origins?(url_1, url_2) when is_binary(url_1) and is_binary(url_2) do
|
||||
uri_1 = URI.parse(url_1)
|
||||
uri_2 = URI.parse(url_2)
|
||||
|
||||
compare_uris?(uri_1, uri_2)
|
||||
end
|
||||
|
||||
defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri), do: id_uri.host == other_uri.host
|
||||
|
||||
@spec origin_check_from_id?(String.t(), String.t()) :: boolean()
|
||||
def origin_check_from_id?(id, other_id) when is_binary(other_id) do
|
||||
id_uri = URI.parse(id)
|
||||
other_uri = URI.parse(other_id)
|
||||
@@ -201,9 +275,20 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
compare_uris?(id_uri, other_uri)
|
||||
end
|
||||
|
||||
@spec origin_check_from_id?(String.t(), map()) :: boolean()
|
||||
def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id),
|
||||
do: origin_check_from_id?(id, other_id)
|
||||
|
||||
def activity_actor_is_group_member?(%Actor{id: actor_id}, object) do
|
||||
case Ownable.group_actor(object) do
|
||||
%Actor{type: :Group, id: group_id} ->
|
||||
Actors.is_member?(actor_id, group_id)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Save picture data from %Plug.Upload{} and return AS Link data.
|
||||
"""
|
||||
@@ -274,7 +359,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
activity_id,
|
||||
public
|
||||
)
|
||||
when type in ["Note", "Event", "ResourceCollection", "Document"] do
|
||||
when type in ["Note", "Event", "ResourceCollection", "Document", "Todo"] do
|
||||
do_make_announce_data(
|
||||
actor,
|
||||
object_actor_url,
|
||||
@@ -367,6 +452,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
"type" => "Create",
|
||||
"to" => object["to"],
|
||||
"cc" => object["cc"],
|
||||
"attributedTo" => object["attributedTo"] || object["actor"],
|
||||
"actor" => object["actor"],
|
||||
"object" => object,
|
||||
"published" => make_date(),
|
||||
@@ -494,7 +580,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
@doc """
|
||||
Sign a request with the instance Relay actor.
|
||||
"""
|
||||
@spec sign_fetch_relay(List.t(), String.t(), String.t()) :: List.t()
|
||||
@spec sign_fetch_relay(Enum.t(), String.t(), String.t()) :: Enum.t()
|
||||
def sign_fetch_relay(headers, id, date) do
|
||||
with %Actor{} = actor <- Relay.get_actor() do
|
||||
sign_fetch(headers, actor, id, date)
|
||||
@@ -504,7 +590,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
@doc """
|
||||
Sign a request with an actor.
|
||||
"""
|
||||
@spec sign_fetch(List.t(), Actor.t(), String.t(), String.t()) :: List.t()
|
||||
@spec sign_fetch(Enum.t(), Actor.t(), String.t(), String.t()) :: Enum.t()
|
||||
def sign_fetch(headers, actor, id, date) do
|
||||
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do
|
||||
headers ++ make_signature(actor, id, date)
|
||||
@@ -516,7 +602,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
@doc """
|
||||
Add the Date header to the request if we sign object fetches
|
||||
"""
|
||||
@spec maybe_date_fetch(List.t(), String.t()) :: List.t()
|
||||
@spec maybe_date_fetch(Enum.t(), String.t()) :: Enum.t()
|
||||
def maybe_date_fetch(headers, date) do
|
||||
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do
|
||||
headers ++ [{:Date, date}]
|
||||
@@ -524,4 +610,15 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
def check_for_actor_key_rotation(%Actor{} = actor) do
|
||||
if Actors.should_rotate_actor_key(actor) do
|
||||
Actors.schedule_key_rotation(
|
||||
actor,
|
||||
Application.get_env(:mobilizon, :activitypub)[:actor_key_rotation_delay]
|
||||
)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ defmodule Mobilizon.Federation.ActivityPub.Visibility do
|
||||
Utility functions related to content visibility
|
||||
"""
|
||||
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
|
||||
|
||||
7
lib/federation/activity_stream.ex
Normal file
7
lib/federation/activity_stream.ex
Normal file
@@ -0,0 +1,7 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream do
|
||||
@moduledoc """
|
||||
The ActivityStream Type
|
||||
"""
|
||||
|
||||
@type t :: map()
|
||||
end
|
||||
@@ -49,7 +49,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
|
||||
banner: banner,
|
||||
name: data["name"],
|
||||
preferred_username: data["preferredUsername"],
|
||||
summary: data["summary"],
|
||||
summary: data["summary"] || "",
|
||||
keys: data["publicKey"]["publicKeyPem"],
|
||||
inbox_url: data["inbox"],
|
||||
outbox_url: data["outbox"],
|
||||
@@ -57,6 +57,10 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
|
||||
followers_url: data["followers"],
|
||||
members_url: data["members"],
|
||||
resources_url: data["resources"],
|
||||
todos_url: data["todos"],
|
||||
events_url: data["events"],
|
||||
posts_url: data["posts"],
|
||||
discussions_url: data["discussions"],
|
||||
shared_inbox_url: data["endpoints"]["sharedInbox"],
|
||||
domain: URI.parse(data["id"]).host,
|
||||
manually_approves_followers: data["manuallyApprovesFollowers"],
|
||||
@@ -77,12 +81,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
|
||||
"type" => actor.type,
|
||||
"preferredUsername" => actor.preferred_username,
|
||||
"name" => actor.name,
|
||||
"summary" => actor.summary,
|
||||
"summary" => actor.summary || "",
|
||||
"following" => actor.following_url,
|
||||
"followers" => actor.followers_url,
|
||||
"members" => actor.members_url,
|
||||
"resources" => actor.resources_url,
|
||||
"todos" => actor.todos_url,
|
||||
"posts" => actor.posts_url,
|
||||
"events" => actor.events_url,
|
||||
"discussions" => actor.discussions_url,
|
||||
"inbox" => actor.inbox_url,
|
||||
"outbox" => actor.outbox_url,
|
||||
"url" => actor.url,
|
||||
|
||||
@@ -7,22 +7,30 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment, as: CommentModel
|
||||
alias Mobilizon.Discussions
|
||||
alias Mobilizon.Discussions.Comment, as: CommentModel
|
||||
alias Mobilizon.Discussions.Discussion
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Tombstone, as: TombstoneModel
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Visibility
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Comment, as: CommentConverter
|
||||
alias Mobilizon.Tombstone, as: TombstoneModel
|
||||
|
||||
import Mobilizon.Federation.ActivityStream.Converter.Utils,
|
||||
only: [
|
||||
fetch_tags: 1,
|
||||
fetch_mentions: 1,
|
||||
build_tags: 1,
|
||||
build_mentions: 1,
|
||||
maybe_fetch_actor_and_attributed_to_id: 1
|
||||
]
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: CommentModel do
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Comment, as: CommentConverter
|
||||
|
||||
defdelegate model_to_as(comment), to: CommentConverter
|
||||
end
|
||||
|
||||
@@ -35,61 +43,35 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
Logger.debug("We're converting raw ActivityStream data to a comment entity")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
||||
{:ok, %Actor{id: actor_id, domain: domain, suspended: false}} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(author_url),
|
||||
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(Map.get(object, "tag", []))},
|
||||
with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <-
|
||||
maybe_fetch_actor_and_attributed_to_id(object),
|
||||
{:tags, tags} <- {:tags, fetch_tags(Map.get(object, "tag", []))},
|
||||
{:mentions, mentions} <-
|
||||
{:mentions, ConverterUtils.fetch_mentions(Map.get(object, "tag", []))} do
|
||||
{:mentions, fetch_mentions(Map.get(object, "tag", []))},
|
||||
discussion <-
|
||||
Discussions.get_discussion_by_url(Map.get(object, "context")) do
|
||||
Logger.debug("Inserting full comment")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
data = %{
|
||||
text: object["content"],
|
||||
url: object["id"],
|
||||
# Will be used in conversations, ignored in basic comments
|
||||
title: object["name"],
|
||||
context: object["context"],
|
||||
actor_id: actor_id,
|
||||
attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id),
|
||||
in_reply_to_comment_id: nil,
|
||||
event_id: nil,
|
||||
uuid: object["uuid"],
|
||||
discussion_id: if(is_nil(discussion), do: nil, else: discussion.id),
|
||||
tags: tags,
|
||||
mentions: mentions,
|
||||
local: is_nil(domain),
|
||||
local: is_nil(actor_domain),
|
||||
visibility: if(Visibility.is_public?(object), do: :public, else: :private)
|
||||
}
|
||||
|
||||
# We fetch the parent object
|
||||
Logger.debug("We're fetching the parent object")
|
||||
|
||||
if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
|
||||
object["inReplyTo"] != "" do
|
||||
Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
|
||||
|
||||
case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
|
||||
# Reply to an event (Event)
|
||||
{:ok, %Event{id: id}} ->
|
||||
Logger.debug("Parent object is an event")
|
||||
data |> Map.put(:event_id, id)
|
||||
|
||||
# Reply to a comment (Comment)
|
||||
{:ok, %CommentModel{id: id} = comment} ->
|
||||
Logger.debug("Parent object is another comment")
|
||||
|
||||
data
|
||||
|> Map.put(:in_reply_to_comment_id, id)
|
||||
|> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
|
||||
|> Map.put(:event_id, comment.event_id)
|
||||
|
||||
# Anything else is kind of a MP
|
||||
{:error, parent} ->
|
||||
Logger.warn("Parent object is something we don't handle")
|
||||
Logger.debug(inspect(parent))
|
||||
data
|
||||
end
|
||||
else
|
||||
Logger.debug("No parent object for this comment")
|
||||
|
||||
data
|
||||
end
|
||||
maybe_fetch_parent_object(object, data)
|
||||
else
|
||||
{:ok, %Actor{suspended: true}} ->
|
||||
:error
|
||||
@@ -102,10 +84,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
@impl Converter
|
||||
@spec model_to_as(CommentModel.t()) :: map
|
||||
def model_to_as(%CommentModel{deleted_at: nil} = comment) do
|
||||
to =
|
||||
if comment.visibility == :public,
|
||||
do: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
else: [comment.actor.followers_url]
|
||||
to = determine_to(comment)
|
||||
|
||||
object = %{
|
||||
"type" => "Note",
|
||||
@@ -114,13 +93,19 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
"content" => comment.text,
|
||||
"mediaType" => "text/html",
|
||||
"actor" => comment.actor.url,
|
||||
"attributedTo" => comment.actor.url,
|
||||
"attributedTo" =>
|
||||
if(is_nil(comment.attributed_to), do: nil, else: comment.attributed_to.url) ||
|
||||
comment.actor.url,
|
||||
"uuid" => comment.uuid,
|
||||
"id" => comment.url,
|
||||
"tag" =>
|
||||
ConverterUtils.build_mentions(comment.mentions) ++ ConverterUtils.build_tags(comment.tags)
|
||||
"tag" => build_mentions(comment.mentions) ++ build_tags(comment.tags)
|
||||
}
|
||||
|
||||
object =
|
||||
if comment.discussion_id,
|
||||
do: Map.put(object, "context", comment.discussion.url),
|
||||
else: object
|
||||
|
||||
cond do
|
||||
comment.in_reply_to_comment ->
|
||||
Map.put(object, "inReplyTo", comment.in_reply_to_comment.url)
|
||||
@@ -133,15 +118,78 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Comment do
|
||||
end
|
||||
end
|
||||
|
||||
@impl Converter
|
||||
@spec model_to_as(CommentModel.t()) :: map
|
||||
@doc """
|
||||
A "soft-deleted" comment is a tombstone
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(CommentModel.t()) :: map
|
||||
def model_to_as(%CommentModel{} = comment) do
|
||||
Convertible.model_to_as(%TombstoneModel{
|
||||
uri: comment.url,
|
||||
inserted_at: comment.deleted_at
|
||||
})
|
||||
end
|
||||
|
||||
@spec determine_to(CommentModel.t()) :: [String.t()]
|
||||
defp determine_to(%CommentModel{} = comment) do
|
||||
cond do
|
||||
not is_nil(comment.attributed_to) ->
|
||||
[comment.attributed_to.url]
|
||||
|
||||
comment.visibility == :public ->
|
||||
["https://www.w3.org/ns/activitystreams#Public"]
|
||||
|
||||
true ->
|
||||
[comment.actor.followers_url]
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_fetch_parent_object(object, data) do
|
||||
# We fetch the parent object
|
||||
Logger.debug("We're fetching the parent object")
|
||||
|
||||
if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
|
||||
object["inReplyTo"] != "" do
|
||||
Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
|
||||
|
||||
case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
|
||||
# Reply to an event (Event)
|
||||
{:ok, %Event{id: id}} ->
|
||||
Logger.debug("Parent object is an event")
|
||||
data |> Map.put(:event_id, id)
|
||||
|
||||
# Reply to a comment (Comment)
|
||||
{:ok, %CommentModel{id: id} = comment} ->
|
||||
Logger.debug("Parent object is another comment")
|
||||
|
||||
data
|
||||
|> Map.put(:in_reply_to_comment_id, id)
|
||||
|> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
|
||||
|> Map.put(:event_id, comment.event_id)
|
||||
|
||||
# Reply to a discucssion (Discussion)
|
||||
{:ok,
|
||||
%Discussion{
|
||||
id: discussion_id,
|
||||
last_comment: %CommentModel{id: last_comment_id, origin_comment_id: origin_comment_id}
|
||||
} = _discussion} ->
|
||||
Logger.debug("Parent object is a discussion")
|
||||
|
||||
data
|
||||
|> Map.put(:in_reply_to_comment_id, last_comment_id)
|
||||
|> Map.put(:origin_comment_id, origin_comment_id)
|
||||
|> Map.put(:discussion_id, discussion_id)
|
||||
|
||||
# Anything else is kind of a MP
|
||||
{:error, parent} ->
|
||||
Logger.warn("Parent object is something we don't handle")
|
||||
Logger.debug(inspect(parent))
|
||||
data
|
||||
end
|
||||
else
|
||||
Logger.debug("No parent object for this comment")
|
||||
|
||||
data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter do
|
||||
one, and back.
|
||||
"""
|
||||
|
||||
@callback as_to_model_data(map) :: map
|
||||
@callback model_to_as(struct) :: map
|
||||
@type model_data :: map()
|
||||
|
||||
@callback as_to_model_data(as_data :: ActivityStream.t()) :: model_data()
|
||||
@callback model_to_as(model :: struct()) :: ActivityStream.t()
|
||||
end
|
||||
|
||||
63
lib/federation/activity_stream/converter/discussion.ex
Normal file
63
lib/federation/activity_stream/converter/discussion.ex
Normal file
@@ -0,0 +1,63 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.Converter.Discussion do
|
||||
@moduledoc """
|
||||
Comment converter.
|
||||
|
||||
This module allows to convert events from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Discussion
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Discussion, as: DiscussionConverter
|
||||
alias Mobilizon.Storage.Repo
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: Discussion do
|
||||
defdelegate model_to_as(comment), to: DiscussionConverter
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an AS comment object from an existing `discussion` structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(Discussion.t()) :: map
|
||||
def model_to_as(%Discussion{} = discussion) do
|
||||
discussion = Repo.preload(discussion, [:last_comment, :actor, :creator])
|
||||
|
||||
%{
|
||||
"type" => "Note",
|
||||
"to" => [discussion.actor.followers_url],
|
||||
"cc" => [],
|
||||
"name" => discussion.title,
|
||||
"content" => discussion.last_comment.text,
|
||||
"mediaType" => "text/html",
|
||||
"actor" => discussion.creator.url,
|
||||
"attributedTo" => discussion.actor.url,
|
||||
"id" => discussion.url,
|
||||
"context" => discussion.url
|
||||
}
|
||||
end
|
||||
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
|
||||
def as_to_model_data(%{"type" => "Note", "name" => name} = object) when not is_nil(name) do
|
||||
with creator_url <- Map.get(object, "actor"),
|
||||
{:ok, %Actor{id: creator_id, suspended: false}} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(creator_url),
|
||||
actor_url <- Map.get(object, "attributedTo"),
|
||||
{:ok, %Actor{id: actor_id, suspended: false}} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(actor_url) do
|
||||
%{
|
||||
title: name,
|
||||
actor_id: actor_id,
|
||||
creator_id: creator_id,
|
||||
url: object["id"]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -12,11 +12,17 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
alias Mobilizon.Events.Event, as: EventModel
|
||||
alias Mobilizon.Media.Picture
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Picture, as: PictureConverter
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
|
||||
import Mobilizon.Federation.ActivityStream.Converter.Utils,
|
||||
only: [
|
||||
fetch_tags: 1,
|
||||
fetch_mentions: 1,
|
||||
build_tags: 1,
|
||||
maybe_fetch_actor_and_attributed_to_id: 1
|
||||
]
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -34,16 +40,12 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map()} | {:error, any()}
|
||||
def as_to_model_data(object) do
|
||||
Logger.debug("event as_to_model_data")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
||||
{:actor, {:ok, %Actor{id: actor_id, domain: actor_domain, suspended: false}}} <-
|
||||
{:actor, ActivityPub.get_or_fetch_actor_by_url(author_url)},
|
||||
with {%Actor{id: actor_id, domain: actor_domain}, attributed_to} <-
|
||||
maybe_fetch_actor_and_attributed_to_id(object),
|
||||
{:address, address_id} <-
|
||||
{:address, get_address(object["location"])},
|
||||
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])},
|
||||
{:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(object["tag"])},
|
||||
{:tags, tags} <- {:tags, fetch_tags(object["tag"])},
|
||||
{:mentions, mentions} <- {:mentions, fetch_mentions(object["tag"])},
|
||||
{:visibility, visibility} <- {:visibility, get_visibility(object)},
|
||||
{:options, options} <- {:options, get_options(object)} do
|
||||
attachments =
|
||||
@@ -67,6 +69,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
title: object["name"],
|
||||
description: object["content"],
|
||||
organizer_actor_id: actor_id,
|
||||
attributed_to_id: if(is_nil(attributed_to), do: nil, else: attributed_to.id),
|
||||
picture_id: picture_id,
|
||||
begins_on: object["startTime"],
|
||||
ends_on: object["endTime"],
|
||||
@@ -108,7 +111,9 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
"type" => "Event",
|
||||
"to" => to,
|
||||
"cc" => [],
|
||||
"attributedTo" => event.organizer_actor.url,
|
||||
"attributedTo" =>
|
||||
if(is_nil(event.attributed_to), do: nil, else: event.attributed_to.url) ||
|
||||
event.organizer_actor.url,
|
||||
"name" => event.title,
|
||||
"actor" => event.organizer_actor.url,
|
||||
"uuid" => event.uuid,
|
||||
@@ -120,7 +125,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
"startTime" => event.begins_on |> date_to_string(),
|
||||
"joinMode" => to_string(event.join_options),
|
||||
"endTime" => event.ends_on |> date_to_string(),
|
||||
"tag" => event.tags |> ConverterUtils.build_tags(),
|
||||
"tag" => event.tags |> build_tags(),
|
||||
"maximumAttendeeCapacity" => event.options.maximum_attendee_capacity,
|
||||
"repliesModerationOption" => event.options.comment_moderation,
|
||||
"commentsEnabled" => event.options.comment_moderation == :allow_all,
|
||||
|
||||
@@ -9,7 +9,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations
|
||||
alias Mobilizon.Discussions
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Reports.Report
|
||||
@@ -92,7 +92,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Flag do
|
||||
Enum.filter(objects, fn url ->
|
||||
!(url == reported.url || (!is_nil(event) && event.url == url))
|
||||
end),
|
||||
comments <- Enum.map(comments, &Conversations.get_comment_from_url/1) do
|
||||
comments <- Enum.map(comments, &Discussions.get_comment_from_url/1) do
|
||||
%{
|
||||
"reporter" => reporter,
|
||||
"uri" => object["id"],
|
||||
|
||||
@@ -39,7 +39,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Picture do
|
||||
actor_id
|
||||
)
|
||||
when is_bitstring(picture_url) do
|
||||
with {:ok, %HTTPoison.Response{body: body}} <- HTTPoison.get(picture_url, [], @http_options),
|
||||
with {:ok, %{body: body}} <- Tesla.get(picture_url, opts: @http_options),
|
||||
{:ok, %{name: name, url: url, content_type: content_type, size: size}} <-
|
||||
Upload.store(%{body: body, name: name}),
|
||||
{:picture_exists, nil} <- {:picture_exists, Media.get_picture_by_url(url)} do
|
||||
|
||||
70
lib/federation/activity_stream/converter/post.ex
Normal file
70
lib/federation/activity_stream/converter/post.ex
Normal file
@@ -0,0 +1,70 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.Converter.Post do
|
||||
@moduledoc """
|
||||
Post converter.
|
||||
|
||||
This module allows to convert posts from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Utils
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
alias Mobilizon.Posts.Post
|
||||
require Logger
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: Post do
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Post, as: PostConverter
|
||||
|
||||
defdelegate model_to_as(post), to: PostConverter
|
||||
end
|
||||
|
||||
@doc """
|
||||
Convert an post struct to an ActivityStream representation
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(Post.t()) :: map
|
||||
def model_to_as(
|
||||
%Post{author: %Actor{url: actor_url}, attributed_to: %Actor{url: creator_url}} = post
|
||||
) do
|
||||
%{
|
||||
"type" => "Article",
|
||||
"actor" => actor_url,
|
||||
"id" => post.url,
|
||||
"name" => post.title,
|
||||
"content" => post.body,
|
||||
"attributedTo" => creator_url,
|
||||
"published" => post.publish_at || post.inserted_at
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
|
||||
def as_to_model_data(
|
||||
%{"type" => "Article", "actor" => creator, "attributedTo" => group} = object
|
||||
) do
|
||||
with {:ok, %Actor{id: attributed_to_id}} <- get_actor(group),
|
||||
{:ok, %Actor{id: author_id}} <- get_actor(creator) do
|
||||
%{
|
||||
title: object["name"],
|
||||
body: object["content"],
|
||||
url: object["id"],
|
||||
attributed_to_id: attributed_to_id,
|
||||
author_id: author_id,
|
||||
local: false,
|
||||
publish_at: object["published"]
|
||||
}
|
||||
else
|
||||
{:error, err} -> {:error, err}
|
||||
err -> {:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_actor(String.t() | map() | nil) :: {:ok, Actor.t()} | {:error, String.t()}
|
||||
defp get_actor(nil), do: {:error, "nil property found for actor data"}
|
||||
defp get_actor(actor), do: actor |> Utils.get_url() |> ActivityPub.get_or_fetch_actor_by_url()
|
||||
end
|
||||
@@ -28,7 +28,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.TodoList do
|
||||
"type" => "TodoList",
|
||||
"actor" => group_url,
|
||||
"id" => todo_list.url,
|
||||
"title" => todo_list.title
|
||||
"name" => todo_list.title
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Tombstone do
|
||||
%{
|
||||
"type" => "Tombstone",
|
||||
"id" => tombstone.uri,
|
||||
"actor" => tombstone.actor.url,
|
||||
"deleted" => tombstone.inserted_at
|
||||
}
|
||||
end
|
||||
|
||||
@@ -23,6 +23,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
||||
tags |> Enum.flat_map(&fetch_tag/1) |> Enum.uniq() |> Enum.map(&existing_tag_or_data/1)
|
||||
end
|
||||
|
||||
def fetch_tags(_), do: []
|
||||
|
||||
@spec fetch_mentions([map()]) :: [map()]
|
||||
def fetch_mentions(mentions) when is_list(mentions) do
|
||||
Logger.debug("fetching mentions")
|
||||
@@ -30,6 +32,8 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
||||
Enum.reduce(mentions, [], fn mention, acc -> create_mention(mention, acc) end)
|
||||
end
|
||||
|
||||
def fetch_mentions(_), do: []
|
||||
|
||||
def fetch_address(%{id: id}) do
|
||||
with {id, ""} <- Integer.parse(id), do: %{id: id}
|
||||
end
|
||||
@@ -38,7 +42,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
||||
address
|
||||
end
|
||||
|
||||
@spec build_tags([Tag.t()]) :: [Map.t()]
|
||||
@spec build_tags([Tag.t()]) :: [map()]
|
||||
def build_tags(tags) do
|
||||
Enum.map(tags, fn %Tag{} = tag ->
|
||||
%{
|
||||
@@ -111,4 +115,51 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
||||
defp create_mention({_, mention}, acc) when is_map(mention) do
|
||||
create_mention(mention, acc)
|
||||
end
|
||||
|
||||
@spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
|
||||
def maybe_fetch_actor_and_attributed_to_id(%{
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => attributed_to_url
|
||||
})
|
||||
when is_nil(attributed_to_url) do
|
||||
{fetch_actor(actor_url), nil}
|
||||
end
|
||||
|
||||
@spec maybe_fetch_actor_and_attributed_to_id(map()) :: {Actor.t() | nil, Actor.t() | nil}
|
||||
def maybe_fetch_actor_and_attributed_to_id(%{
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => attributed_to_url
|
||||
})
|
||||
when is_nil(actor_url) do
|
||||
{fetch_actor(attributed_to_url), nil}
|
||||
end
|
||||
|
||||
# Only when both actor and attributedTo fields are both filled is when we can return both
|
||||
def maybe_fetch_actor_and_attributed_to_id(%{
|
||||
"actor" => actor_url,
|
||||
"attributedTo" => attributed_to_url
|
||||
})
|
||||
when actor_url != attributed_to_url do
|
||||
with actor <- fetch_actor(actor_url),
|
||||
attributed_to <- fetch_actor(attributed_to_url) do
|
||||
{actor, attributed_to}
|
||||
end
|
||||
end
|
||||
|
||||
# If we only have attributedTo and no actor, take attributedTo as the actor
|
||||
def maybe_fetch_actor_and_attributed_to_id(%{
|
||||
"attributedTo" => attributed_to_url
|
||||
}) do
|
||||
{fetch_actor(attributed_to_url), nil}
|
||||
end
|
||||
|
||||
def maybe_fetch_actor_and_attributed_to_id(_), do: {nil, nil}
|
||||
|
||||
@spec fetch_actor(String.t()) :: Actor.t()
|
||||
defp fetch_actor(actor_url) do
|
||||
with {:ok, %Actor{suspended: false} = actor} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(actor_url) do
|
||||
actor
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,8 +3,9 @@ defprotocol Mobilizon.Federation.ActivityStream.Convertible do
|
||||
Convertible protocol.
|
||||
"""
|
||||
|
||||
@type activity_streams :: map
|
||||
@type t :: struct()
|
||||
@type activity_streams :: map()
|
||||
|
||||
@spec model_to_as(t) :: activity_streams
|
||||
@spec model_to_as(t()) :: activity_streams()
|
||||
def model_to_as(convertible)
|
||||
end
|
||||
|
||||
@@ -118,13 +118,15 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||
Logger.debug(inspect(address))
|
||||
|
||||
with false <- is_nil(domain),
|
||||
{:ok, %HTTPoison.Response{} = response} <-
|
||||
HTTPoison.get(
|
||||
{:ok, %{} = response} <-
|
||||
Tesla.get(
|
||||
address,
|
||||
[Accept: "application/json, application/activity+json, application/jrd+json"],
|
||||
@http_options
|
||||
headers: [
|
||||
{"accept", "application/json, application/activity+json, application/jrd+json"}
|
||||
],
|
||||
opts: @http_options
|
||||
),
|
||||
%{status_code: status_code, body: body} when status_code in 200..299 <- response,
|
||||
%{status: status, body: body} when status in 200..299 <- response,
|
||||
{:ok, doc} <- Jason.decode(body) do
|
||||
webfinger_from_json(doc)
|
||||
else
|
||||
|
||||
@@ -3,8 +3,8 @@ defmodule Mobilizon.GraphQL.API.Comments do
|
||||
API for Comments.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Conversations.Comment
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Activity
|
||||
|
||||
@@ -19,7 +19,7 @@ defmodule Mobilizon.GraphQL.API.Comments do
|
||||
end
|
||||
|
||||
def update_comment(%Comment{} = comment, args) do
|
||||
ActivityPub.update(:comment, comment, args, true)
|
||||
ActivityPub.update(comment, args, true)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -27,8 +27,8 @@ defmodule Mobilizon.GraphQL.API.Comments do
|
||||
|
||||
Deletes a comment from an actor
|
||||
"""
|
||||
@spec delete_comment(Comment.t()) :: {:ok, Activity.t(), Comment.t()} | any
|
||||
def delete_comment(%Comment{} = comment) do
|
||||
ActivityPub.delete(comment, true)
|
||||
@spec delete_comment(Comment.t(), Actor.t()) :: {:ok, Activity.t(), Comment.t()} | any
|
||||
def delete_comment(%Comment{} = comment, %Actor{} = actor) do
|
||||
ActivityPub.delete(comment, actor, true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,7 +34,7 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||
Map.update(args, :picture, nil, fn picture ->
|
||||
process_picture(picture, organizer_actor)
|
||||
end) do
|
||||
ActivityPub.update(:event, event, args, Map.get(args, :draft, false) == false)
|
||||
ActivityPub.update(event, args, Map.get(args, :draft, false) == false)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,8 +43,8 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||
|
||||
If the event is deleted by
|
||||
"""
|
||||
def delete_event(%Event{} = event, federate \\ true) do
|
||||
ActivityPub.delete(event, federate)
|
||||
def delete_event(%Event{} = event, %Actor{} = actor, federate \\ true) do
|
||||
ActivityPub.delete(event, actor, federate)
|
||||
end
|
||||
|
||||
defp process_picture(nil, _), do: nil
|
||||
|
||||
@@ -19,8 +19,25 @@ defmodule Mobilizon.GraphQL.API.Groups do
|
||||
args |> Map.get(:preferred_username) |> HTML.strip_tags() |> String.trim(),
|
||||
{:existing_group, nil} <-
|
||||
{:existing_group, Actors.get_local_group_by_title(preferred_username)},
|
||||
args <- args |> Map.put(:type, :Group),
|
||||
{:ok, %Activity{} = activity, %Actor{} = group} <-
|
||||
ActivityPub.create(:group, args, true, %{"actor" => args.creator_actor.url}) do
|
||||
ActivityPub.create(:actor, args, true, %{"actor" => args.creator_actor.url}) do
|
||||
{:ok, activity, group}
|
||||
else
|
||||
{:existing_group, _} ->
|
||||
{:error, "A group with this name already exists"}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Actor id is not owned by authenticated user"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec create_group(map) :: {:ok, Activity.t(), Actor.t()} | any
|
||||
def update_group(%{id: id} = args) do
|
||||
with {:existing_group, {:ok, %Actor{type: :Group} = group}} <-
|
||||
{:existing_group, Actors.get_group_by_actor_id(id)},
|
||||
{:ok, %Activity{} = activity, %Actor{} = group} <-
|
||||
ActivityPub.update(group, args, true, %{"actor" => args.updater_actor.url}) do
|
||||
{:ok, activity, group}
|
||||
else
|
||||
{:existing_group, _} ->
|
||||
|
||||
@@ -9,7 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Admin.{ActionLog, Setting}
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Relay
|
||||
@@ -297,7 +297,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
|
||||
|
||||
with {:changes, true} <- {:changes, args != %{}},
|
||||
%Actor{} = instance_actor <- Relay.get_actor(),
|
||||
{:ok, _activity, _actor} <- ActivityPub.update(:actor, instance_actor, args, true) do
|
||||
{:ok, _activity, _actor} <- ActivityPub.update(instance_actor, args, true) do
|
||||
:ok
|
||||
else
|
||||
{:changes, false} ->
|
||||
|
||||
@@ -3,9 +3,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
|
||||
Handles the comment-related GraphQL calls.
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Admin, Conversations}
|
||||
alias Mobilizon.{Actors, Admin, Discussions}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment, as: CommentModel
|
||||
alias Mobilizon.Discussions.Comment, as: CommentModel
|
||||
alias Mobilizon.Users
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@@ -14,7 +14,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
|
||||
require Logger
|
||||
|
||||
def get_thread(_parent, %{id: thread_id}, _context) do
|
||||
{:ok, Conversations.get_thread_replies(thread_id)}
|
||||
{:ok, Discussions.get_thread_replies(thread_id)}
|
||||
end
|
||||
|
||||
def create_comment(
|
||||
@@ -51,7 +51,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
%CommentModel{actor_id: comment_actor_id} = comment <-
|
||||
Mobilizon.Conversations.get_comment(comment_id),
|
||||
Mobilizon.Discussions.get_comment(comment_id),
|
||||
true <- actor_id === comment_actor_id,
|
||||
{:ok, _, %CommentModel{} = comment} <- Comments.update_comment(comment, %{text: text}) do
|
||||
{:ok, comment}
|
||||
@@ -72,15 +72,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
|
||||
}
|
||||
) do
|
||||
with {actor_id, ""} <- Integer.parse(actor_id),
|
||||
{:is_owned, %Actor{} = _organizer_actor} <- User.owns_actor(user, actor_id),
|
||||
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
|
||||
%CommentModel{deleted_at: nil} = comment <-
|
||||
Conversations.get_comment_with_preload(comment_id) do
|
||||
Discussions.get_comment_with_preload(comment_id) do
|
||||
cond do
|
||||
{:comment_can_be_managed, true} == CommentModel.can_be_managed_by(comment, actor_id) ->
|
||||
do_delete_comment(comment)
|
||||
do_delete_comment(comment, actor)
|
||||
|
||||
role in [:moderator, :administrator] ->
|
||||
with {:ok, res} <- do_delete_comment(comment),
|
||||
with {:ok, res} <- do_delete_comment(comment, actor),
|
||||
%Actor{} = actor <- Actors.get_actor(actor_id) do
|
||||
Admin.log_action(actor, "delete", comment)
|
||||
|
||||
@@ -103,9 +103,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Comment do
|
||||
{:error, "You are not allowed to delete a comment if not connected"}
|
||||
end
|
||||
|
||||
defp do_delete_comment(%CommentModel{} = comment) do
|
||||
defp do_delete_comment(%CommentModel{} = comment, %Actor{} = actor) do
|
||||
with {:ok, _, %CommentModel{} = comment} <-
|
||||
Comments.delete_comment(comment) do
|
||||
Comments.delete_comment(comment, actor) do
|
||||
{:ok, comment}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
defmodule Mobilizon.GraphQL.Resolvers.Conversation do
|
||||
@moduledoc """
|
||||
Handles the group-related GraphQL calls.
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Conversations, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Conversation, as: ConversationModel
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
def find_conversations_for_actor(
|
||||
%Actor{id: group_id},
|
||||
_args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
|
||||
{:ok, Conversations.find_conversations_for_actor(group_id)}
|
||||
else
|
||||
{:member, false} ->
|
||||
{:ok, %Page{total: 0, elements: []}}
|
||||
end
|
||||
end
|
||||
|
||||
def find_conversations_for_actor(%Actor{}, _args, _resolution) do
|
||||
{:ok, %Page{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
def get_conversation(_parent, %{id: id}, _resolution) do
|
||||
{:ok, Conversations.get_conversation(id)}
|
||||
end
|
||||
|
||||
def get_comments_for_conversation(
|
||||
%ConversationModel{id: conversation_id},
|
||||
%{page: page, limit: limit},
|
||||
_resolution
|
||||
) do
|
||||
{:ok, Conversations.get_comments_for_conversation(conversation_id, page, limit)}
|
||||
end
|
||||
|
||||
def create_conversation(
|
||||
_parent,
|
||||
%{title: title, text: text, actor_id: actor_id, creator_id: creator_id},
|
||||
_resolution
|
||||
) do
|
||||
with {:ok, %ConversationModel{} = conversation} <-
|
||||
Conversations.create_conversation(%{
|
||||
title: title,
|
||||
text: text,
|
||||
actor_id: actor_id,
|
||||
creator_id: creator_id
|
||||
}) do
|
||||
{:ok, conversation}
|
||||
end
|
||||
end
|
||||
|
||||
def reply_to_conversation(
|
||||
_parent,
|
||||
%{text: text, conversation_id: conversation_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:no_conversation, %ConversationModel{} = conversation} <-
|
||||
{:no_conversation, Conversations.get_conversation(conversation_id)},
|
||||
{:ok, %ConversationModel{} = conversation} <-
|
||||
Conversations.reply_to_conversation(
|
||||
conversation,
|
||||
%{
|
||||
text: text,
|
||||
actor_id: actor_id
|
||||
}
|
||||
) do
|
||||
{:ok, conversation}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_conversation(map(), map(), map()) :: {:ok, ConversationModel.t()}
|
||||
def update_conversation(
|
||||
_parent,
|
||||
%{title: title, conversation_id: conversation_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:no_conversation, %ConversationModel{creator_id: creator_id} = conversation} <-
|
||||
{:no_conversation, Conversations.get_conversation(conversation_id)},
|
||||
{:check_access, true} <- {:check_access, actor_id == creator_id},
|
||||
{:ok, %ConversationModel{} = conversation} <-
|
||||
Conversations.update_conversation(
|
||||
conversation,
|
||||
%{
|
||||
title: title
|
||||
}
|
||||
) do
|
||||
{:ok, conversation}
|
||||
end
|
||||
end
|
||||
end
|
||||
179
lib/graphql/resolvers/discussion.ex
Normal file
179
lib/graphql/resolvers/discussion.ex
Normal file
@@ -0,0 +1,179 @@
|
||||
defmodule Mobilizon.GraphQL.Resolvers.Discussion do
|
||||
@moduledoc """
|
||||
Handles the group-related GraphQL calls.
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Discussions, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
def find_discussions_for_actor(
|
||||
%Actor{id: group_id},
|
||||
_args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
|
||||
{:ok, Discussions.find_discussions_for_actor(group_id)}
|
||||
else
|
||||
{:member, false} ->
|
||||
{:ok, %Page{total: 0, elements: []}}
|
||||
end
|
||||
end
|
||||
|
||||
def find_discussions_for_actor(%Actor{}, _args, _resolution) do
|
||||
{:ok, %Page{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
def get_discussion(_parent, %{id: id}, %{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
%Discussion{actor_id: actor_id} = discussion <-
|
||||
Discussions.get_discussion(id),
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do
|
||||
{:ok, discussion}
|
||||
end
|
||||
end
|
||||
|
||||
def get_discussion(_parent, %{slug: slug}, %{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
%Discussion{actor_id: actor_id} = discussion <-
|
||||
Discussions.get_discussion_by_slug(slug),
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)} do
|
||||
{:ok, discussion}
|
||||
else
|
||||
nil -> {:error, "No such discussion"}
|
||||
end
|
||||
end
|
||||
|
||||
def get_discussion(_parent, _args, _resolution),
|
||||
do: {:error, "You need to be logged-in to access discussions"}
|
||||
|
||||
def get_comments_for_discussion(
|
||||
%Discussion{id: discussion_id},
|
||||
%{page: page, limit: limit},
|
||||
_resolution
|
||||
) do
|
||||
{:ok, Discussions.get_comments_for_discussion(discussion_id, page, limit)}
|
||||
end
|
||||
|
||||
def create_discussion(
|
||||
_parent,
|
||||
%{title: title, text: text, actor_id: actor_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
ActivityPub.create(
|
||||
:discussion,
|
||||
%{
|
||||
title: title,
|
||||
text: text,
|
||||
actor_id: actor_id,
|
||||
creator_id: creator_id,
|
||||
attributed_to_id: actor_id
|
||||
},
|
||||
true
|
||||
) do
|
||||
{:ok, discussion}
|
||||
end
|
||||
end
|
||||
|
||||
def reply_to_discussion(
|
||||
_parent,
|
||||
%{text: text, discussion_id: discussion_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:no_discussion,
|
||||
%Discussion{
|
||||
actor_id: actor_id,
|
||||
last_comment: %Comment{
|
||||
id: last_comment_id,
|
||||
origin_comment_id: origin_comment_id,
|
||||
in_reply_to_comment_id: previous_in_reply_to_comment_id
|
||||
}
|
||||
} = _discussion} <-
|
||||
{:no_discussion, Discussions.get_discussion(discussion_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
ActivityPub.create(
|
||||
:discussion,
|
||||
%{
|
||||
text: text,
|
||||
discussion_id: discussion_id,
|
||||
actor_id: creator_id,
|
||||
attributed_to_id: actor_id,
|
||||
in_reply_to_comment_id: last_comment_id,
|
||||
origin_comment_id:
|
||||
origin_comment_id || previous_in_reply_to_comment_id || last_comment_id
|
||||
},
|
||||
true
|
||||
) do
|
||||
{:ok, discussion}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_discussion(map(), map(), map()) :: {:ok, Discussion.t()}
|
||||
def update_discussion(
|
||||
_parent,
|
||||
%{title: title, discussion_id: discussion_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with {:actor, %Actor{id: creator_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
|
||||
{:no_discussion, Discussions.get_discussion(discussion_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
ActivityPub.update(
|
||||
discussion,
|
||||
%{
|
||||
title: title
|
||||
}
|
||||
) do
|
||||
{:ok, discussion}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_discussion(_parent, %{discussion_id: discussion_id}, %{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}) do
|
||||
with {:actor, %Actor{id: creator_id} = actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||
{:no_discussion, %Discussion{actor_id: actor_id} = discussion} <-
|
||||
{:no_discussion, Discussions.get_discussion(discussion_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(creator_id, actor_id)},
|
||||
{:ok, _activity, %Discussion{} = discussion} <-
|
||||
ActivityPub.delete(discussion, actor) do
|
||||
{:ok, discussion}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -255,13 +255,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||
) do
|
||||
with {:ok, %Event{local: is_local} = event} <- Events.get_event_with_preload(event_id),
|
||||
{actor_id, ""} <- Integer.parse(actor_id),
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id) do
|
||||
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id) do
|
||||
cond do
|
||||
{:event_can_be_managed, true} == Event.can_be_managed_by(event, actor_id) ->
|
||||
do_delete_event(event)
|
||||
do_delete_event(event, actor)
|
||||
|
||||
role in [:moderator, :administrator] ->
|
||||
with {:ok, res} <- do_delete_event(event, !is_local),
|
||||
with {:ok, res} <- do_delete_event(event, actor, !is_local),
|
||||
%Actor{} = actor <- Actors.get_actor(actor_id) do
|
||||
Admin.log_action(actor, "delete", event)
|
||||
|
||||
@@ -284,8 +284,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||
{:error, "You need to be logged-in to delete an event"}
|
||||
end
|
||||
|
||||
defp do_delete_event(event, federate \\ true) when is_boolean(federate) do
|
||||
with {:ok, _activity, event} <- API.Events.delete_event(event) do
|
||||
defp do_delete_event(%Event{} = event, %Actor{} = actor, federate \\ true)
|
||||
when is_boolean(federate) do
|
||||
with {:ok, _activity, event} <- API.Events.delete_event(event, actor) do
|
||||
{:ok, %{id: event.id}}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -80,7 +80,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||
API.Groups.create_group(args) do
|
||||
{:ok, group}
|
||||
else
|
||||
{:error, err} when is_bitstring(err) ->
|
||||
{:error, err} when is_binary(err) ->
|
||||
{:error, err}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
@@ -92,6 +92,36 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||
{:error, "You need to be logged-in to create a group"}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Create a new group. The creator is automatically added as admin
|
||||
"""
|
||||
def update_group(
|
||||
_parent,
|
||||
args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
}
|
||||
) do
|
||||
with %Actor{} = updater_actor <- Users.get_actor_for_user(user),
|
||||
args <- Map.put(args, :updater_actor, updater_actor),
|
||||
{:ok, _activity, %Actor{type: :Group} = group} <-
|
||||
API.Groups.update_group(args) do
|
||||
{:ok, group}
|
||||
else
|
||||
{:error, err} when is_binary(err) ->
|
||||
{:error, err}
|
||||
|
||||
{:is_owned, nil} ->
|
||||
{:error, "Creator actor id is not owned by the current user"}
|
||||
end
|
||||
end
|
||||
|
||||
def update_group(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to update a group"}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete an existing group
|
||||
"""
|
||||
|
||||
@@ -16,14 +16,26 @@ defmodule Mobilizon.GraphQL.Resolvers.Member do
|
||||
"""
|
||||
def find_members_for_group(
|
||||
%Actor{id: group_id} = group,
|
||||
_args,
|
||||
%{page: page, limit: limit, roles: roles},
|
||||
%{
|
||||
context: %{current_user: %User{} = user}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
%Page{} = page <- Actors.list_members_for_group(group) do
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
|
||||
roles =
|
||||
case roles do
|
||||
"" ->
|
||||
[]
|
||||
|
||||
roles ->
|
||||
roles
|
||||
|> String.split(",")
|
||||
|> Enum.map(&String.downcase/1)
|
||||
|> Enum.map(&String.to_existing_atom/1)
|
||||
end
|
||||
|
||||
%Page{} = page = Actors.list_members_for_group(group, roles, page, limit)
|
||||
{:ok, page}
|
||||
else
|
||||
{:member, false} ->
|
||||
|
||||
@@ -129,7 +129,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
|
||||
{:find_actor, Actors.get_actor(id)},
|
||||
{:is_owned, %Actor{}} <- User.owns_actor(user, actor.id),
|
||||
args <- save_attached_pictures(args),
|
||||
{:ok, _activity, %Actor{} = actor} <- ActivityPub.update(:actor, actor, args, true) do
|
||||
{:ok, _activity, %Actor{} = actor} <- ActivityPub.update(actor, args, true) do
|
||||
{:ok, actor}
|
||||
else
|
||||
{:find_actor, nil} ->
|
||||
|
||||
198
lib/graphql/resolvers/post.ex
Normal file
198
lib/graphql/resolvers/post.ex
Normal file
@@ -0,0 +1,198 @@
|
||||
defmodule Mobilizon.GraphQL.Resolvers.Post do
|
||||
@moduledoc """
|
||||
Handles the posts-related GraphQL calls
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Posts, Users}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Storage.Page
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
require Logger
|
||||
|
||||
@public_accessible_visibilities [:public, :unlisted]
|
||||
|
||||
@doc """
|
||||
Find posts for group.
|
||||
|
||||
Returns only if actor requesting is a member of the group
|
||||
"""
|
||||
def find_posts_for_group(
|
||||
%Actor{id: group_id} = group,
|
||||
%{page: page, limit: limit} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
%Page{} = page <- Posts.get_posts_for_group(group, page, limit) do
|
||||
{:ok, page}
|
||||
else
|
||||
{:member, _} ->
|
||||
find_posts_for_group(group, args, nil)
|
||||
end
|
||||
end
|
||||
|
||||
def find_posts_for_group(
|
||||
%Actor{} = group,
|
||||
%{page: page, limit: limit},
|
||||
_resolution
|
||||
) do
|
||||
with %Page{} = page <- Posts.get_public_posts_for_group(group, page, limit) do
|
||||
{:ok, page}
|
||||
end
|
||||
end
|
||||
|
||||
def find_posts_for_group(
|
||||
_group,
|
||||
_args,
|
||||
_resolution
|
||||
) do
|
||||
{:ok, %Page{total: 0, elements: []}}
|
||||
end
|
||||
|
||||
def get_post(
|
||||
parent,
|
||||
%{slug: slug},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:current_actor, %Actor{id: actor_id}} <-
|
||||
{:current_actor, Users.get_actor_for_user(user)},
|
||||
{:post, %Post{attributed_to: %Actor{id: group_id}} = post} <-
|
||||
{:post, Posts.get_post_by_slug_with_preloads(slug)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)} do
|
||||
{:ok, post}
|
||||
else
|
||||
{:member, false} -> get_post(parent, %{slug: slug}, nil)
|
||||
{:post, _} -> {:error, "No such post"}
|
||||
end
|
||||
end
|
||||
|
||||
def get_post(
|
||||
_parent,
|
||||
%{slug: slug},
|
||||
_resolution
|
||||
) do
|
||||
case {:post, Posts.get_post_by_slug_with_preloads(slug)} do
|
||||
{:post, %Post{visibility: visibility, draft: false} = post}
|
||||
when visibility in @public_accessible_visibilities ->
|
||||
{:ok, post}
|
||||
|
||||
{:post, _} ->
|
||||
{:error, "No such post"}
|
||||
end
|
||||
end
|
||||
|
||||
def get_post(_parent, _args, _resolution) do
|
||||
{:error, "No such post"}
|
||||
end
|
||||
|
||||
def create_post(
|
||||
_parent,
|
||||
%{attributed_to_id: group_id} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Post{} = post} <-
|
||||
ActivityPub.create(
|
||||
:post,
|
||||
args
|
||||
|> Map.put(:author_id, actor_id)
|
||||
|> Map.put(:attributed_to_id, group_id),
|
||||
true,
|
||||
%{}
|
||||
) do
|
||||
{:ok, post}
|
||||
else
|
||||
{:own_check, _} ->
|
||||
{:error, "Parent post doesn't match this group"}
|
||||
|
||||
{:member, _} ->
|
||||
{:error, "Actor id is not member of group"}
|
||||
end
|
||||
end
|
||||
|
||||
def create_post(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to create posts"}
|
||||
end
|
||||
|
||||
def update_post(
|
||||
_parent,
|
||||
%{id: id} = args,
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(id)},
|
||||
%Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
{:post, %Post{attributed_to: %Actor{id: group_id}} = post} <-
|
||||
{:post, Posts.get_post_with_preloads(id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Post{} = post} <-
|
||||
ActivityPub.update(post, args, true, %{}) do
|
||||
{:ok, post}
|
||||
else
|
||||
{:uuid, :error} ->
|
||||
{:error, "Post ID is not a valid ID"}
|
||||
|
||||
{:post, _} ->
|
||||
{:error, "Post doesn't exist"}
|
||||
|
||||
{:member, _} ->
|
||||
{:error, "Actor id is not member of group"}
|
||||
end
|
||||
end
|
||||
|
||||
def update_post(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to update posts"}
|
||||
end
|
||||
|
||||
def delete_post(
|
||||
_parent,
|
||||
%{id: post_id},
|
||||
%{
|
||||
context: %{
|
||||
current_user: %User{} = user
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(post_id)},
|
||||
%Actor{id: actor_id} = actor <- Users.get_actor_for_user(user),
|
||||
{:post, %Post{attributed_to: %Actor{id: group_id}} = post} <-
|
||||
{:post, Posts.get_post_with_preloads(post_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Post{} = post} <-
|
||||
ActivityPub.delete(post, actor) do
|
||||
{:ok, post}
|
||||
else
|
||||
{:uuid, :error} ->
|
||||
{:error, "Post ID is not a valid ID"}
|
||||
|
||||
{:post, _} ->
|
||||
{:error, "Post doesn't exist"}
|
||||
|
||||
{:member, _} ->
|
||||
{:error, "Actor id is not member of group"}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_post(_parent, _args, _resolution) do
|
||||
{:error, "You need to be logged-in to delete posts"}
|
||||
end
|
||||
end
|
||||
@@ -141,7 +141,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
|
||||
{:resource, Resources.get_resource_with_preloads(resource_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Resource{} = resource} <-
|
||||
ActivityPub.update(:resource, resource, args, true, %{}) do
|
||||
ActivityPub.update(resource, args, true, %{}) do
|
||||
{:ok, resource}
|
||||
else
|
||||
{:resource, _} ->
|
||||
@@ -165,12 +165,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
|
||||
}
|
||||
} = _resolution
|
||||
) do
|
||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||
with %Actor{id: actor_id} = actor <- Users.get_actor_for_user(user),
|
||||
{:resource, %Resource{parent_id: _parent_id, actor_id: group_id} = resource} <-
|
||||
{:resource, Resources.get_resource_with_preloads(resource_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Resource{} = resource} <-
|
||||
ActivityPub.delete(resource) do
|
||||
ActivityPub.delete(resource, actor) do
|
||||
{:ok, resource}
|
||||
else
|
||||
{:resource, _} ->
|
||||
|
||||
@@ -3,8 +3,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
|
||||
Handles the tag-related GraphQL calls
|
||||
"""
|
||||
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.{Events, Posts}
|
||||
alias Mobilizon.Events.{Event, Tag}
|
||||
alias Mobilizon.Posts.Post
|
||||
|
||||
def list_tags(_parent, %{page: page, limit: limit}, _resolution) do
|
||||
tags = Mobilizon.Events.list_tags(page, limit)
|
||||
@@ -16,7 +17,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
|
||||
Retrieve the list of tags for an event
|
||||
"""
|
||||
def list_tags_for_event(%Event{id: id}, _args, _resolution) do
|
||||
{:ok, Mobilizon.Events.list_tags_for_event(id)}
|
||||
{:ok, Events.list_tags_for_event(id)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -24,10 +25,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
|
||||
"""
|
||||
def list_tags_for_event(%{url: url}, _args, _resolution) do
|
||||
with %Event{id: event_id} <- Events.get_event_by_url(url) do
|
||||
{:ok, Mobilizon.Events.list_tags_for_event(event_id)}
|
||||
{:ok, Events.list_tags_for_event(event_id)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Retrieve the list of tags for a post
|
||||
"""
|
||||
def list_tags_for_post(%Post{id: id}, _args, _resolution) do
|
||||
{:ok, Posts.list_tags_for_post(id)}
|
||||
end
|
||||
|
||||
# @doc """
|
||||
# Retrieve the list of related tags for a given tag ID
|
||||
# """
|
||||
@@ -42,7 +50,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Tag do
|
||||
Retrieve the list of related tags for a parent tag
|
||||
"""
|
||||
def get_related_tags(%Tag{} = tag, _args, _resolution) do
|
||||
with tags <- Mobilizon.Events.list_tag_neighbors(tag) do
|
||||
with tags <- Events.list_tag_neighbors(tag) do
|
||||
{:ok, tags}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -211,7 +211,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Todos do
|
||||
{:todo_list, Todos.get_todo_list(todo_list_id)},
|
||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
|
||||
{:ok, _, %Todo{} = todo} <-
|
||||
ActivityPub.update(:todo, todo, args, true, %{}) do
|
||||
ActivityPub.update(todo, args, true, %{}) do
|
||||
{:ok, todo}
|
||||
else
|
||||
{:todo_list, _} ->
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Crypto
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Relay
|
||||
alias Mobilizon.Service.Auth.Authenticator
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
alias Mobilizon.Users.{Setting, User}
|
||||
@@ -417,7 +418,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
with {:moderator_actor, %Actor{} = moderator_actor} <-
|
||||
{:moderator_actor, Users.get_actor_for_user(moderator_user)},
|
||||
%User{disabled: false} = user <- Users.get_user(user_id),
|
||||
{:ok, %User{}} <- do_delete_account(%User{} = user) do
|
||||
{:ok, %User{}} <-
|
||||
do_delete_account(%User{} = user, Relay.get_actor()) do
|
||||
Admin.log_action(moderator_actor, "delete", user)
|
||||
else
|
||||
{:moderator_actor, nil} ->
|
||||
@@ -432,7 +434,7 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
{:error, "You need to be logged-in to delete your account"}
|
||||
end
|
||||
|
||||
defp do_delete_account(%User{} = user) do
|
||||
defp do_delete_account(%User{} = user, actor_performing \\ nil) do
|
||||
with actors <- Users.get_actors_for_user(user),
|
||||
activated <- not is_nil(user.confirmed_at),
|
||||
# Detach actors from user
|
||||
@@ -444,7 +446,8 @@ defmodule Mobilizon.GraphQL.Resolvers.User do
|
||||
# Launch a background job to delete actors
|
||||
:ok <-
|
||||
Enum.each(actors, fn actor ->
|
||||
ActivityPub.delete(actor, true)
|
||||
actor_performing = actor_performing || actor
|
||||
ActivityPub.delete(actor, actor_performing, true)
|
||||
end),
|
||||
# Delete user
|
||||
{:ok, user} <- Users.delete_user(user, reserve_email: activated) do
|
||||
|
||||
@@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
alias Mobilizon.{
|
||||
Actors,
|
||||
Addresses,
|
||||
Conversations,
|
||||
Discussions,
|
||||
Events,
|
||||
Media,
|
||||
Reports,
|
||||
@@ -18,7 +18,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
}
|
||||
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
alias Mobilizon.Storage.Repo
|
||||
@@ -34,10 +34,11 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
import_types(Schema.Actors.PersonType)
|
||||
import_types(Schema.Actors.GroupType)
|
||||
import_types(Schema.Actors.ApplicationType)
|
||||
import_types(Schema.Conversations.CommentType)
|
||||
import_types(Schema.Conversations.ConversationType)
|
||||
import_types(Schema.Discussions.CommentType)
|
||||
import_types(Schema.Discussions.DiscussionType)
|
||||
import_types(Schema.SearchType)
|
||||
import_types(Schema.ResourceType)
|
||||
import_types(Schema.PostType)
|
||||
import_types(Schema.Todos.TodoListType)
|
||||
import_types(Schema.Todos.TodoType)
|
||||
import_types(Schema.ConfigType)
|
||||
@@ -116,7 +117,7 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
|> Dataloader.add_source(Actors, default_source)
|
||||
|> Dataloader.add_source(Users, default_source)
|
||||
|> Dataloader.add_source(Events, default_source)
|
||||
|> Dataloader.add_source(Conversations, Conversations.data())
|
||||
|> Dataloader.add_source(Discussions, Discussions.data())
|
||||
|> Dataloader.add_source(Addresses, default_source)
|
||||
|> Dataloader.add_source(Media, default_source)
|
||||
|> Dataloader.add_source(Reports, default_source)
|
||||
@@ -148,8 +149,9 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
import_fields(:admin_queries)
|
||||
import_fields(:todo_list_queries)
|
||||
import_fields(:todo_queries)
|
||||
import_fields(:conversation_queries)
|
||||
import_fields(:discussion_queries)
|
||||
import_fields(:resource_queries)
|
||||
import_fields(:post_queries)
|
||||
import_fields(:statistics_queries)
|
||||
end
|
||||
|
||||
@@ -170,8 +172,9 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
import_fields(:admin_mutations)
|
||||
import_fields(:todo_list_mutations)
|
||||
import_fields(:todo_mutations)
|
||||
import_fields(:conversation_mutations)
|
||||
import_fields(:discussion_mutations)
|
||||
import_fields(:resource_mutations)
|
||||
import_fields(:post_mutations)
|
||||
end
|
||||
|
||||
@desc """
|
||||
@@ -179,5 +182,6 @@ defmodule Mobilizon.GraphQL.Schema do
|
||||
"""
|
||||
subscription do
|
||||
import_fields(:person_subscriptions)
|
||||
import_fields(:discussion_subscriptions)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
alias Mobilizon.GraphQL.Resolvers.{Conversation, Group, Member, Resource, Todos}
|
||||
alias Mobilizon.GraphQL.Resolvers.{Discussion, Group, Member, Post, Resource, Todos}
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
import_types(Schema.Actors.MemberType)
|
||||
@@ -46,9 +46,9 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
description("A list of the events this actor has organized")
|
||||
end
|
||||
|
||||
field :conversations, :paginated_conversation_list do
|
||||
resolve(&Conversation.find_conversations_for_actor/3)
|
||||
description("A list of the conversations for this group")
|
||||
field :discussions, :paginated_discussion_list do
|
||||
resolve(&Discussion.find_discussions_for_actor/3)
|
||||
description("A list of the discussions for this group")
|
||||
end
|
||||
|
||||
field(:types, :group_type, description: "The type of group : Group, Community,…")
|
||||
@@ -58,8 +58,11 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
)
|
||||
|
||||
field :members, :paginated_member_list do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
arg(:roles, :string, default_value: "")
|
||||
resolve(&Member.find_members_for_group/3)
|
||||
description("List of group members")
|
||||
description("A paginated list of group members")
|
||||
end
|
||||
|
||||
field :resources, :paginated_resource_list do
|
||||
@@ -69,6 +72,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
description("A paginated list of the resources this group has")
|
||||
end
|
||||
|
||||
field :posts, :paginated_post_list do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
resolve(&Post.find_posts_for_group/3)
|
||||
description("A paginated list of the posts this group has")
|
||||
end
|
||||
|
||||
field :todo_lists, :paginated_todo_list_list do
|
||||
resolve(&Todos.find_todo_lists_for_group/3)
|
||||
description("A paginated list of the todo lists this group has")
|
||||
@@ -99,6 +109,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
field(:total, :integer, description: "The total number of elements in the list")
|
||||
end
|
||||
|
||||
@desc "The list of visibility options for a group"
|
||||
enum :group_visibility do
|
||||
value(:public, description: "Publicly listed and federated")
|
||||
value(:unlisted, description: "Visible only to people with the link - or invited")
|
||||
end
|
||||
|
||||
object :group_queries do
|
||||
@desc "Get all groups"
|
||||
field :groups, :paginated_group_list do
|
||||
@@ -124,6 +140,11 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
arg(:name, :string, description: "The displayed name for the group")
|
||||
arg(:summary, :string, description: "The summary for the group", default_value: "")
|
||||
|
||||
arg(:visibility, :group_visibility,
|
||||
description: "The visibility for the group",
|
||||
default_value: :public
|
||||
)
|
||||
|
||||
arg(:avatar, :picture_input,
|
||||
description:
|
||||
"The avatar for the group, either as an object or directly the ID of an existing Picture"
|
||||
@@ -137,6 +158,26 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||
resolve(&Group.create_group/3)
|
||||
end
|
||||
|
||||
@desc "Update a group"
|
||||
field :update_group, :group do
|
||||
arg(:id, non_null(:id), description: "The group ID")
|
||||
|
||||
arg(:name, :string, description: "The displayed name for the group")
|
||||
arg(:summary, :string, description: "The summary for the group", default_value: "")
|
||||
|
||||
arg(:avatar, :picture_input,
|
||||
description:
|
||||
"The avatar for the group, either as an object or directly the ID of an existing Picture"
|
||||
)
|
||||
|
||||
arg(:banner, :picture_input,
|
||||
description:
|
||||
"The banner for the group, either as an object or directly the ID of an existing Picture"
|
||||
)
|
||||
|
||||
resolve(&Group.update_group/3)
|
||||
end
|
||||
|
||||
@desc "Delete a group"
|
||||
field :delete_group, :deleted_object do
|
||||
arg(:group_id, non_null(:id))
|
||||
|
||||
@@ -15,6 +15,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
|
||||
field(:actor, :person, description: "Which profile is member of")
|
||||
field(:role, :member_role_enum, description: "The role of this membership")
|
||||
field(:invited_by, :person, description: "Who invited this member")
|
||||
field(:inserted_at, :naive_datetime, description: "When was this member created")
|
||||
end
|
||||
|
||||
enum :member_role_enum do
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Reports.{Note, Report}
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
defmodule Mobilizon.GraphQL.Schema.Conversations.ConversationType do
|
||||
@moduledoc """
|
||||
Schema representation for Conversation
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.GraphQL.Resolvers.Conversation
|
||||
|
||||
@desc "A conversation"
|
||||
object :conversation do
|
||||
field(:id, :id, description: "Internal ID for this conversation")
|
||||
field(:title, :string)
|
||||
field(:slug, :string)
|
||||
field(:last_comment, :comment)
|
||||
|
||||
field :comments, :paginated_comment_list do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
resolve(&Conversation.get_comments_for_conversation/3)
|
||||
description("The comments for the conversation")
|
||||
end
|
||||
|
||||
field(:creator, :person, resolve: dataloader(Actors))
|
||||
field(:actor, :actor, resolve: dataloader(Actors))
|
||||
field(:inserted_at, :datetime)
|
||||
field(:updated_at, :datetime)
|
||||
end
|
||||
|
||||
object :paginated_conversation_list do
|
||||
field(:elements, list_of(:conversation), description: "A list of conversation")
|
||||
field(:total, :integer, description: "The total number of comments in the list")
|
||||
end
|
||||
|
||||
object :conversation_queries do
|
||||
@desc "Get a conversation"
|
||||
field :conversation, type: :conversation do
|
||||
arg(:id, non_null(:id))
|
||||
resolve(&Conversation.get_conversation/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :conversation_mutations do
|
||||
@desc "Create a conversation"
|
||||
field :create_conversation, type: :conversation do
|
||||
arg(:title, non_null(:string))
|
||||
arg(:text, non_null(:string))
|
||||
arg(:actor_id, non_null(:id))
|
||||
arg(:creator_id, non_null(:id))
|
||||
|
||||
resolve(&Conversation.create_conversation/3)
|
||||
end
|
||||
|
||||
field :reply_to_conversation, type: :conversation do
|
||||
arg(:conversation_id, non_null(:id))
|
||||
arg(:text, non_null(:string))
|
||||
resolve(&Conversation.reply_to_conversation/3)
|
||||
end
|
||||
|
||||
field :update_conversation, type: :conversation do
|
||||
arg(:title, non_null(:string))
|
||||
arg(:conversation_id, non_null(:id))
|
||||
resolve(&Conversation.update_conversation/3)
|
||||
end
|
||||
|
||||
field :delete_conversation, type: :conversation do
|
||||
arg(:conversation_id, non_null(:id))
|
||||
|
||||
# resolve(&Conversation.delete_conversation/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule Mobilizon.GraphQL.Schema.Conversations.CommentType do
|
||||
defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
|
||||
@moduledoc """
|
||||
Schema representation for Comment
|
||||
"""
|
||||
@@ -6,7 +6,7 @@ defmodule Mobilizon.GraphQL.Schema.Conversations.CommentType do
|
||||
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.{Actors, Conversations}
|
||||
alias Mobilizon.{Actors, Discussions}
|
||||
alias Mobilizon.GraphQL.Resolvers.Comment
|
||||
|
||||
@desc "A comment"
|
||||
@@ -21,13 +21,13 @@ defmodule Mobilizon.GraphQL.Schema.Conversations.CommentType do
|
||||
field(:primaryLanguage, :string)
|
||||
|
||||
field(:replies, list_of(:comment)) do
|
||||
resolve(dataloader(Conversations))
|
||||
resolve(dataloader(Discussions))
|
||||
end
|
||||
|
||||
field(:total_replies, :integer)
|
||||
field(:in_reply_to_comment, :comment, resolve: dataloader(Conversations))
|
||||
field(:in_reply_to_comment, :comment, resolve: dataloader(Discussions))
|
||||
field(:event, :event, resolve: dataloader(Events))
|
||||
field(:origin_comment, :comment, resolve: dataloader(Conversations))
|
||||
field(:origin_comment, :comment, resolve: dataloader(Discussions))
|
||||
field(:threadLanguages, non_null(list_of(:string)))
|
||||
field(:actor, :person, resolve: dataloader(Actors))
|
||||
field(:inserted_at, :datetime)
|
||||
85
lib/graphql/schema/discussions/discussion.ex
Normal file
85
lib/graphql/schema/discussions/discussion.ex
Normal file
@@ -0,0 +1,85 @@
|
||||
defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
|
||||
@moduledoc """
|
||||
Schema representation for discussion
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.GraphQL.Resolvers.Discussion
|
||||
|
||||
@desc "A discussion"
|
||||
object :discussion do
|
||||
field(:id, :id, description: "Internal ID for this discussion")
|
||||
field(:title, :string)
|
||||
field(:slug, :string)
|
||||
field(:last_comment, :comment)
|
||||
|
||||
field :comments, :paginated_comment_list do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
resolve(&Discussion.get_comments_for_discussion/3)
|
||||
description("The comments for the discussion")
|
||||
end
|
||||
|
||||
field(:creator, :person, resolve: dataloader(Actors))
|
||||
field(:actor, :actor, resolve: dataloader(Actors))
|
||||
field(:inserted_at, :datetime)
|
||||
field(:updated_at, :datetime)
|
||||
end
|
||||
|
||||
object :paginated_discussion_list do
|
||||
field(:elements, list_of(:discussion), description: "A list of discussion")
|
||||
field(:total, :integer, description: "The total number of comments in the list")
|
||||
end
|
||||
|
||||
object :discussion_queries do
|
||||
@desc "Get a discussion"
|
||||
field :discussion, type: :discussion do
|
||||
arg(:id, :id)
|
||||
arg(:slug, :string)
|
||||
resolve(&Discussion.get_discussion/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :discussion_mutations do
|
||||
@desc "Create a discussion"
|
||||
field :create_discussion, type: :discussion do
|
||||
arg(:title, non_null(:string))
|
||||
arg(:text, non_null(:string))
|
||||
arg(:actor_id, non_null(:id))
|
||||
arg(:creator_id, non_null(:id))
|
||||
|
||||
resolve(&Discussion.create_discussion/3)
|
||||
end
|
||||
|
||||
field :reply_to_discussion, type: :discussion do
|
||||
arg(:discussion_id, non_null(:id))
|
||||
arg(:text, non_null(:string))
|
||||
resolve(&Discussion.reply_to_discussion/3)
|
||||
end
|
||||
|
||||
field :update_discussion, type: :discussion do
|
||||
arg(:title, non_null(:string))
|
||||
arg(:discussion_id, non_null(:id))
|
||||
resolve(&Discussion.update_discussion/3)
|
||||
end
|
||||
|
||||
field :delete_discussion, type: :discussion do
|
||||
arg(:discussion_id, non_null(:id))
|
||||
|
||||
resolve(&Discussion.delete_discussion/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :discussion_subscriptions do
|
||||
field :discussion_comment_changed, :discussion do
|
||||
arg(:slug, non_null(:string))
|
||||
|
||||
config(fn args, _ ->
|
||||
{:ok, topic: args.slug}
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,7 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
import Mobilizon.GraphQL.Helpers.Error
|
||||
|
||||
alias Mobilizon.{Actors, Addresses, Conversations}
|
||||
alias Mobilizon.{Actors, Addresses, Discussions}
|
||||
alias Mobilizon.GraphQL.Resolvers.{Event, Picture, Tag}
|
||||
alias Mobilizon.GraphQL.Schema
|
||||
|
||||
@@ -82,7 +82,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||
)
|
||||
|
||||
field(:comments, list_of(:comment), description: "The comments in reply to the event") do
|
||||
resolve(dataloader(Conversations))
|
||||
resolve(dataloader(Discussions))
|
||||
end
|
||||
|
||||
# field(:tracks, list_of(:track))
|
||||
|
||||
91
lib/graphql/schema/post.ex
Normal file
91
lib/graphql/schema/post.ex
Normal file
@@ -0,0 +1,91 @@
|
||||
defmodule Mobilizon.GraphQL.Schema.PostType do
|
||||
@moduledoc """
|
||||
Schema representation for Posts
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
alias Mobilizon.GraphQL.Resolvers.{Post, Tag}
|
||||
|
||||
@desc "A post"
|
||||
object :post do
|
||||
field(:id, :id, description: "The post's ID")
|
||||
field(:title, :string, description: "The post's title")
|
||||
field(:slug, :string, description: "The post's slug")
|
||||
field(:body, :string, description: "The post's body, as HTML")
|
||||
field(:url, :string, description: "The post's URL")
|
||||
field(:draft, :boolean, description: "Whether the post is a draft")
|
||||
field(:author, :actor, description: "The post's author")
|
||||
field(:attributed_to, :actor, description: "The post's group")
|
||||
field(:visibility, :post_visibility, description: "The post's visibility")
|
||||
field(:publish_at, :datetime, description: "When the post was published")
|
||||
field(:inserted_at, :naive_datetime, description: "The post's creation date")
|
||||
field(:updated_at, :naive_datetime, description: "The post's last update date")
|
||||
|
||||
field(:tags, list_of(:tag),
|
||||
resolve: &Tag.list_tags_for_post/3,
|
||||
description: "The post's tags"
|
||||
)
|
||||
end
|
||||
|
||||
object :paginated_post_list do
|
||||
field(:elements, list_of(:post), description: "A list of posts")
|
||||
field(:total, :integer, description: "The total number of posts in the list")
|
||||
end
|
||||
|
||||
@desc "The list of visibility options for a post"
|
||||
enum :post_visibility do
|
||||
value(:public, description: "Publicly listed and federated. Can be shared.")
|
||||
value(:unlisted, description: "Visible only to people with the link")
|
||||
# value(:restricted, description: "Visible only after a moderator accepted")
|
||||
|
||||
value(:private,
|
||||
description: "Visible only to people members of the group or followers of the person"
|
||||
)
|
||||
end
|
||||
|
||||
object :post_queries do
|
||||
@desc "Get a post"
|
||||
field :post, :post do
|
||||
arg(:slug, non_null(:string))
|
||||
resolve(&Post.get_post/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :post_mutations do
|
||||
@desc "Create a post"
|
||||
field :create_post, :post do
|
||||
arg(:attributed_to_id, non_null(:id))
|
||||
arg(:title, non_null(:string))
|
||||
arg(:body, :string)
|
||||
arg(:draft, :boolean, default_value: false)
|
||||
arg(:visibility, :post_visibility)
|
||||
arg(:publish_at, :datetime)
|
||||
|
||||
arg(:tags, list_of(:string),
|
||||
default_value: [],
|
||||
description: "The list of tags associated to the post"
|
||||
)
|
||||
|
||||
resolve(&Post.create_post/3)
|
||||
end
|
||||
|
||||
@desc "Update a post"
|
||||
field :update_post, :post do
|
||||
arg(:id, non_null(:id))
|
||||
arg(:title, :string)
|
||||
arg(:body, :string)
|
||||
arg(:attributed_to_id, :id)
|
||||
arg(:draft, :boolean)
|
||||
arg(:visibility, :post_visibility)
|
||||
arg(:publish_at, :datetime)
|
||||
arg(:tags, list_of(:string), description: "The list of tags associated to the post")
|
||||
|
||||
resolve(&Post.update_post/3)
|
||||
end
|
||||
|
||||
@desc "Delete a post"
|
||||
field :delete_post, :deleted_object do
|
||||
arg(:id, non_null(:id))
|
||||
resolve(&Post.delete_post/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||
|
||||
alias Mobilizon.{Actors, Config, Crypto, Mention, Share}
|
||||
alias Mobilizon.Actors.{ActorOpenness, ActorType, ActorVisibility, Follower, Member}
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.{Event, FeedToken}
|
||||
alias Mobilizon.Media.File
|
||||
alias Mobilizon.Reports.{Note, Report}
|
||||
@@ -27,6 +27,9 @@ defmodule Mobilizon.Actors.Actor do
|
||||
following_url: String.t(),
|
||||
followers_url: String.t(),
|
||||
shared_inbox_url: String.t(),
|
||||
resources_url: String.t(),
|
||||
posts_url: String.t(),
|
||||
events_url: String.t(),
|
||||
type: ActorType.t(),
|
||||
name: String.t(),
|
||||
domain: String.t(),
|
||||
@@ -62,6 +65,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:shared_inbox_url,
|
||||
:following_url,
|
||||
:followers_url,
|
||||
:posts_url,
|
||||
:events_url,
|
||||
:todos_url,
|
||||
:discussions_url,
|
||||
:type,
|
||||
:name,
|
||||
:domain,
|
||||
@@ -96,6 +103,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:followers_url,
|
||||
:members_url,
|
||||
:resources_url,
|
||||
:posts_url,
|
||||
:todos_url,
|
||||
:events_url,
|
||||
:discussions_url,
|
||||
:name,
|
||||
:summary,
|
||||
:manually_approves_followers,
|
||||
@@ -117,6 +128,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||
|
||||
schema "actors" do
|
||||
field(:url, :string)
|
||||
|
||||
field(:outbox_url, :string)
|
||||
field(:inbox_url, :string)
|
||||
field(:following_url, :string)
|
||||
@@ -124,7 +136,11 @@ defmodule Mobilizon.Actors.Actor do
|
||||
field(:shared_inbox_url, :string)
|
||||
field(:members_url, :string)
|
||||
field(:resources_url, :string)
|
||||
field(:posts_url, :string)
|
||||
field(:events_url, :string)
|
||||
field(:todos_url, :string)
|
||||
field(:discussions_url, :string)
|
||||
|
||||
field(:type, ActorType, default: :Person)
|
||||
field(:name, :string)
|
||||
field(:domain, :string, default: nil)
|
||||
@@ -344,7 +360,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||
def build_url("relay", :page, _args),
|
||||
do: Endpoint |> Routes.activity_pub_url(:relay) |> URI.decode()
|
||||
|
||||
def build_url(preferred_username, endpoint, args) when endpoint in [:page, :resources] do
|
||||
def build_url(preferred_username, endpoint, args)
|
||||
when endpoint in [:page, :resources, :posts, :discussions, :events, :todos] do
|
||||
endpoint = if endpoint == :page, do: :actor, else: endpoint
|
||||
|
||||
Endpoint
|
||||
@@ -353,7 +370,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||
end
|
||||
|
||||
def build_url(preferred_username, endpoint, args)
|
||||
when endpoint in [:outbox, :following, :followers, :members, :todos] do
|
||||
when endpoint in [:outbox, :following, :followers, :members] do
|
||||
Endpoint
|
||||
|> Routes.activity_pub_url(endpoint, preferred_username, args)
|
||||
|> URI.decode()
|
||||
|
||||
@@ -55,6 +55,8 @@ defmodule Mobilizon.Actors do
|
||||
|
||||
@public_visibility [:public, :unlisted]
|
||||
@administrator_roles [:creator, :administrator]
|
||||
@moderator_roles [:moderator] ++ @administrator_roles
|
||||
@member_roles [:member] ++ @moderator_roles
|
||||
@actor_preloads [:user, :organized_events, :comments]
|
||||
|
||||
@doc """
|
||||
@@ -118,6 +120,17 @@ defmodule Mobilizon.Actors do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
New function to replace `Mobilizon.Actors.get_actor_by_url/1` with
|
||||
better signature
|
||||
"""
|
||||
@spec get_actor_by_url_2(String.t(), boolean) :: Actor.t() | nil
|
||||
def get_actor_by_url_2(url, preload \\ false) do
|
||||
Actor
|
||||
|> Repo.get_by(url: url)
|
||||
|> preload_followers(preload)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets an actor by its URL (ActivityPub ID). The `:preload` option allows to
|
||||
preload the followers relation.
|
||||
@@ -181,9 +194,17 @@ defmodule Mobilizon.Actors do
|
||||
"""
|
||||
@spec create_actor(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_actor(attrs \\ %{}) do
|
||||
%Actor{}
|
||||
|> Actor.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
type = Map.get(attrs, :type, :Person)
|
||||
|
||||
case type do
|
||||
:Person ->
|
||||
%Actor{}
|
||||
|> Actor.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
|
||||
:Group ->
|
||||
create_group(attrs)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -238,7 +259,8 @@ defmodule Mobilizon.Actors do
|
||||
name: name,
|
||||
summary: summary,
|
||||
avatar: transform_media_file(avatar),
|
||||
banner: transform_media_file(banner)
|
||||
banner: transform_media_file(banner),
|
||||
last_refreshed_at: DateTime.utc_now()
|
||||
]
|
||||
],
|
||||
conflict_target: [:url]
|
||||
@@ -285,6 +307,7 @@ defmodule Mobilizon.Actors do
|
||||
"""
|
||||
@spec perform(atom(), Actor.t()) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def perform(:delete_actor, %Actor{} = actor, options \\ @delete_actor_default_options) do
|
||||
Logger.info("Going to delete actor #{actor.url}")
|
||||
actor = Repo.preload(actor, @actor_preloads)
|
||||
|
||||
delete_actor_options = Keyword.merge(@delete_actor_default_options, options)
|
||||
@@ -306,10 +329,18 @@ defmodule Mobilizon.Actors do
|
||||
case Repo.transaction(multi) do
|
||||
{:ok, %{actor: %Actor{} = actor}} ->
|
||||
{:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}")
|
||||
Logger.info("Deleted actor #{actor.url}")
|
||||
{:ok, actor}
|
||||
|
||||
{:error, remove, error, _} when remove in [:remove_banner, :remove_avatar] ->
|
||||
Logger.error("Error while deleting actor's banner or avatar")
|
||||
Logger.error(inspect(error, pretty: true))
|
||||
{:error, error}
|
||||
|
||||
err ->
|
||||
Logger.error("Unknown error while deleting actor")
|
||||
Logger.error(inspect(err, pretty: true))
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -438,23 +469,47 @@ defmodule Mobilizon.Actors do
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_local_group_by_url(String.t()) :: Actor.t()
|
||||
def get_local_group_by_url(group_url) do
|
||||
group_query()
|
||||
|> where([q], q.url == ^group_url and is_nil(q.domain))
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@spec get_group_by_members_url(String.t()) :: Actor.t()
|
||||
def get_group_by_members_url(members_url) do
|
||||
group_query()
|
||||
|> where([q], q.members_url == ^members_url)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a group.
|
||||
|
||||
If the group is local, creates an admin actor as well from `creator_actor_id`.
|
||||
"""
|
||||
@spec create_group(map) :: {:ok, Actor.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_group(attrs \\ %{}) do
|
||||
with {:ok, %{insert_group: %Actor{} = group, add_admin_member: %Member{} = _admin_member}} <-
|
||||
Multi.new()
|
||||
|> Multi.insert(:insert_group, Actor.group_creation_changeset(%Actor{}, attrs))
|
||||
|> Multi.insert(:add_admin_member, fn %{insert_group: group} ->
|
||||
Member.changeset(%Member{}, %{
|
||||
parent_id: group.id,
|
||||
actor_id: attrs.creator_actor_id,
|
||||
role: :administrator
|
||||
})
|
||||
end)
|
||||
|> Repo.transaction() do
|
||||
{:ok, group}
|
||||
local = Map.get(attrs, :local, true)
|
||||
|
||||
if local do
|
||||
with {:ok, %{insert_group: %Actor{} = group, add_admin_member: %Member{} = _admin_member}} <-
|
||||
Multi.new()
|
||||
|> Multi.insert(:insert_group, Actor.group_creation_changeset(%Actor{}, attrs))
|
||||
|> Multi.insert(:add_admin_member, fn %{insert_group: group} ->
|
||||
Member.changeset(%Member{}, %{
|
||||
parent_id: group.id,
|
||||
actor_id: attrs.creator_actor_id,
|
||||
role: :administrator
|
||||
})
|
||||
end)
|
||||
|> Repo.transaction() do
|
||||
{:ok, group}
|
||||
end
|
||||
else
|
||||
%Actor{}
|
||||
|> Actor.group_creation_changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -532,12 +587,7 @@ defmodule Mobilizon.Actors do
|
||||
def is_member?(actor_id, parent_id) do
|
||||
match?(
|
||||
{:ok, %Member{}},
|
||||
get_member(actor_id, parent_id, [
|
||||
:member,
|
||||
:moderator,
|
||||
:administrator,
|
||||
:creator
|
||||
])
|
||||
get_member(actor_id, parent_id, @member_roles)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -552,6 +602,20 @@ defmodule Mobilizon.Actors do
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@spec get_single_group_member_actor(integer() | String.t()) :: Actor.t() | nil
|
||||
def get_single_group_member_actor(group_id) do
|
||||
Member
|
||||
|> where(
|
||||
[m],
|
||||
m.parent_id == ^group_id and m.role in [^:member, ^:moderator, ^:administrator, ^:creator]
|
||||
)
|
||||
|> join(:inner, [m], a in Actor, on: m.actor_id == a.id)
|
||||
|> where([_m, a], is_nil(a.domain))
|
||||
|> limit(1)
|
||||
|> select([_m, a], a)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a member.
|
||||
"""
|
||||
@@ -616,25 +680,26 @@ defmodule Mobilizon.Actors do
|
||||
@doc """
|
||||
Returns the list of members for a group.
|
||||
"""
|
||||
@spec list_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_members_for_group(%Actor{id: group_id, type: :Group}, page \\ nil, limit \\ nil) do
|
||||
group_id
|
||||
|> members_for_group_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@spec list_external_members_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_external_members_for_group(
|
||||
@spec list_members_for_group(Actor.t(), list(atom()), integer | nil, integer | nil) :: Page.t()
|
||||
def list_members_for_group(
|
||||
%Actor{id: group_id, type: :Group},
|
||||
roles \\ [],
|
||||
page \\ nil,
|
||||
limit \\ nil
|
||||
) do
|
||||
group_id
|
||||
|> members_for_group_query()
|
||||
|> filter_external()
|
||||
|> filter_member_role(roles)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@spec list_external_actors_members_for_group(Actor.t()) :: list(Actor.t())
|
||||
def list_external_actors_members_for_group(%Actor{id: group_id, type: :Group}) do
|
||||
group_id
|
||||
|> group_external_member_actor_query()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the list of administrator members for a group.
|
||||
"""
|
||||
@@ -1141,6 +1206,26 @@ defmodule Mobilizon.Actors do
|
||||
)
|
||||
end
|
||||
|
||||
@spec group_external_member_actor_query(integer()) :: Ecto.Query.t()
|
||||
defp group_external_member_actor_query(group_id) do
|
||||
Member
|
||||
|> where([m], m.parent_id == ^group_id)
|
||||
|> join(:inner, [m], a in Actor, on: m.actor_id == a.id)
|
||||
|> where([_m, a], not is_nil(a.domain))
|
||||
|> select([_m, a], a)
|
||||
end
|
||||
|
||||
@spec filter_member_role(Ecto.Query.t(), list(atom()) | atom()) :: Ecto.Query.t()
|
||||
def filter_member_role(query, []), do: query
|
||||
|
||||
def filter_member_role(query, roles) when is_list(roles) do
|
||||
where(query, [m], m.role in ^roles)
|
||||
end
|
||||
|
||||
def filter_member_role(query, role) when is_atom(role) do
|
||||
from(m in query, where: m.role == ^role)
|
||||
end
|
||||
|
||||
@spec administrator_members_for_group_query(integer | String.t()) :: Ecto.Query.t()
|
||||
defp administrator_members_for_group_query(group_id) do
|
||||
from(
|
||||
@@ -1296,13 +1381,22 @@ defmodule Mobilizon.Actors do
|
||||
defp preload_followers(actor, true), do: Repo.preload(actor, [:followers])
|
||||
defp preload_followers(actor, false), do: actor
|
||||
|
||||
defp delete_actor_organized_events(%Actor{organized_events: organized_events}) do
|
||||
defp delete_actor_organized_events(%Actor{organized_events: organized_events} = actor) do
|
||||
res =
|
||||
Enum.map(organized_events, fn event ->
|
||||
event =
|
||||
Repo.preload(event, [:organizer_actor, :participants, :picture, :mentions, :comments])
|
||||
Repo.preload(event, [
|
||||
:organizer_actor,
|
||||
:participants,
|
||||
:picture,
|
||||
:mentions,
|
||||
:comments,
|
||||
:attributed_to,
|
||||
:tags,
|
||||
:physical_address
|
||||
])
|
||||
|
||||
ActivityPub.delete(event, false)
|
||||
ActivityPub.delete(event, actor, false)
|
||||
end)
|
||||
|
||||
if Enum.all?(res, fn {status, _, _} -> status == :ok end) do
|
||||
@@ -1312,13 +1406,21 @@ defmodule Mobilizon.Actors do
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_actor_empty_comments(%Actor{comments: comments}) do
|
||||
defp delete_actor_empty_comments(%Actor{comments: comments} = actor) do
|
||||
res =
|
||||
Enum.map(comments, fn comment ->
|
||||
comment =
|
||||
Repo.preload(comment, [:actor, :mentions, :event, :in_reply_to_comment, :origin_comment])
|
||||
Repo.preload(comment, [
|
||||
:actor,
|
||||
:mentions,
|
||||
:event,
|
||||
:in_reply_to_comment,
|
||||
:origin_comment,
|
||||
:attributed_to,
|
||||
:tags
|
||||
])
|
||||
|
||||
ActivityPub.delete(comment, false)
|
||||
ActivityPub.delete(comment, actor, false)
|
||||
end)
|
||||
|
||||
if Enum.all?(res, fn {status, _, _} -> status == :ok end) do
|
||||
|
||||
@@ -119,7 +119,7 @@ defmodule Mobilizon.Config do
|
||||
|
||||
@spec instance_user_agent :: String.t()
|
||||
def instance_user_agent,
|
||||
do: "#{instance_name()} #{instance_hostname()} - Mobilizon #{instance_version()}"
|
||||
do: "#{instance_hostname()} - Mobilizon #{instance_version()}"
|
||||
|
||||
@spec instance_federating :: String.t()
|
||||
def instance_federating, do: instance_config()[:federating]
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
defmodule Mobilizon.Conversations.Conversation.TitleSlug do
|
||||
@moduledoc """
|
||||
Module to generate the slug for conversations
|
||||
"""
|
||||
use EctoAutoslugField.Slug, from: :title, to: :slug
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Conversations.Conversation do
|
||||
@moduledoc """
|
||||
Represents a conversation
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Conversations.Conversation.TitleSlug
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
creator: Actor.t(),
|
||||
actor: Actor.t(),
|
||||
title: String.t(),
|
||||
slug: String.t(),
|
||||
last_comment: Comment.t(),
|
||||
comments: list(Comment.t())
|
||||
}
|
||||
|
||||
@required_attrs [:actor_id, :creator_id, :title, :last_comment_id]
|
||||
@optional_attrs []
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
schema "conversations" do
|
||||
field(:title, :string)
|
||||
field(:slug, TitleSlug.Type)
|
||||
belongs_to(:creator, Actor)
|
||||
belongs_to(:actor, Actor)
|
||||
belongs_to(:last_comment, Comment)
|
||||
has_many(:comments, Comment, foreign_key: :conversation_id)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = conversation, attrs) do
|
||||
conversation
|
||||
|> cast(attrs, @attrs)
|
||||
|> validate_required(@required_attrs)
|
||||
|> TitleSlug.maybe_generate_slug()
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule Mobilizon.Conversations.Comment do
|
||||
defmodule Mobilizon.Discussions.Comment do
|
||||
@moduledoc """
|
||||
Represents an actor comment (for instance on an event or on a group).
|
||||
"""
|
||||
@@ -8,7 +8,7 @@ defmodule Mobilizon.Conversations.Comment do
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.{Comment, CommentVisibility, Conversation}
|
||||
alias Mobilizon.Discussions.{Comment, CommentVisibility, Discussion}
|
||||
alias Mobilizon.Events.{Event, Tag}
|
||||
alias Mobilizon.Mention
|
||||
|
||||
@@ -42,7 +42,7 @@ defmodule Mobilizon.Conversations.Comment do
|
||||
:attributed_to_id,
|
||||
:deleted_at,
|
||||
:local,
|
||||
:conversation_id
|
||||
:discussion_id
|
||||
]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@@ -60,7 +60,7 @@ defmodule Mobilizon.Conversations.Comment do
|
||||
belongs_to(:event, Event, foreign_key: :event_id)
|
||||
belongs_to(:in_reply_to_comment, Comment, foreign_key: :in_reply_to_comment_id)
|
||||
belongs_to(:origin_comment, Comment, foreign_key: :origin_comment_id)
|
||||
belongs_to(:conversation, Conversation)
|
||||
belongs_to(:discussion, Discussion, type: :binary_id)
|
||||
has_many(:replies, Comment, foreign_key: :in_reply_to_comment_id)
|
||||
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
|
||||
has_many(:mentions, Mention)
|
||||
@@ -69,7 +69,7 @@ defmodule Mobilizon.Conversations.Comment do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the id of the first comment in the conversation.
|
||||
Returns the id of the first comment in the discussion.
|
||||
"""
|
||||
@spec get_thread_id(t) :: integer
|
||||
def get_thread_id(%__MODULE__{id: id, origin_comment_id: origin_comment_id}) do
|
||||
@@ -98,6 +98,7 @@ defmodule Mobilizon.Conversations.Comment do
|
||||
|> change()
|
||||
|> put_change(:text, nil)
|
||||
|> put_change(:actor_id, nil)
|
||||
|> put_change(:discussion_id, nil)
|
||||
|> put_change(:deleted_at, DateTime.utc_now() |> DateTime.truncate(:second))
|
||||
end
|
||||
|
||||
102
lib/mobilizon/discussions/discussion.ex
Normal file
102
lib/mobilizon/discussions/discussion.ex
Normal file
@@ -0,0 +1,102 @@
|
||||
defmodule Mobilizon.Discussions.Discussion.TitleSlug do
|
||||
@moduledoc """
|
||||
Module to generate the slug for discussions
|
||||
"""
|
||||
use EctoAutoslugField.Slug, from: [:title, :id], to: :slug
|
||||
|
||||
def build_slug([title, id], %Ecto.Changeset{valid?: true}) do
|
||||
[title, ShortUUID.encode!(id)]
|
||||
|> Enum.join("-")
|
||||
|> Slugger.slugify()
|
||||
end
|
||||
|
||||
def build_slug(_sources, %Ecto.Changeset{valid?: false}), do: ""
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Discussions.Discussion do
|
||||
@moduledoc """
|
||||
Represents a discussion
|
||||
"""
|
||||
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Discussions.Discussion.TitleSlug
|
||||
alias Mobilizon.Web.Endpoint
|
||||
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
creator: Actor.t(),
|
||||
actor: Actor.t(),
|
||||
title: String.t(),
|
||||
url: String.t(),
|
||||
slug: String.t(),
|
||||
last_comment: Comment.t(),
|
||||
comments: list(Comment.t())
|
||||
}
|
||||
|
||||
@required_attrs [:actor_id, :creator_id, :title, :last_comment_id, :url, :id]
|
||||
@optional_attrs []
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@primary_key {:id, Ecto.UUID, autogenerate: true}
|
||||
|
||||
schema "discussions" do
|
||||
field(:title, :string)
|
||||
field(:slug, TitleSlug.Type)
|
||||
field(:url, :string)
|
||||
belongs_to(:creator, Actor)
|
||||
belongs_to(:actor, Actor)
|
||||
belongs_to(:last_comment, Comment)
|
||||
has_many(:comments, Comment, foreign_key: :discussion_id)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec changeset(t, map) :: Ecto.Changeset.t()
|
||||
def changeset(%__MODULE__{} = discussion, attrs) do
|
||||
discussion
|
||||
|> cast(attrs, @attrs)
|
||||
|> maybe_generate_id()
|
||||
|> validate_required([:title, :id])
|
||||
|> TitleSlug.maybe_generate_slug()
|
||||
|> TitleSlug.unique_constraint()
|
||||
|> maybe_generate_url()
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
|
||||
defp maybe_generate_id(%Ecto.Changeset{} = changeset) do
|
||||
case fetch_field(changeset, :id) do
|
||||
res when res in [:error, {:data, nil}] ->
|
||||
put_change(changeset, :id, Ecto.UUID.generate())
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
@spec maybe_generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp maybe_generate_url(%Ecto.Changeset{} = changeset) do
|
||||
with res when res in [:error, {:data, nil}] <- fetch_field(changeset, :url),
|
||||
{changes, slug} when changes in [:changes, :data] <-
|
||||
fetch_field(changeset, :slug),
|
||||
{_changes, actor_id} <-
|
||||
fetch_field(changeset, :actor_id),
|
||||
%Actor{preferred_username: preferred_username} <-
|
||||
Actors.get_actor(actor_id),
|
||||
url <- generate_url(preferred_username, slug) do
|
||||
put_change(changeset, :url, url)
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
@spec generate_url(String.t(), String.t()) :: String.t()
|
||||
defp generate_url(preferred_username, slug),
|
||||
do: Routes.page_url(Endpoint, :discussion, preferred_username, slug)
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule Mobilizon.Conversations do
|
||||
defmodule Mobilizon.Discussions do
|
||||
@moduledoc """
|
||||
The conversations context
|
||||
The discussions context
|
||||
"""
|
||||
|
||||
import EctoEnum
|
||||
@@ -9,7 +9,7 @@ defmodule Mobilizon.Conversations do
|
||||
alias Ecto.Changeset
|
||||
alias Ecto.Multi
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.{Comment, Conversation}
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
|
||||
defenum(
|
||||
@@ -42,10 +42,11 @@ defmodule Mobilizon.Conversations do
|
||||
:origin_comment,
|
||||
:replies,
|
||||
:tags,
|
||||
:mentions
|
||||
:mentions,
|
||||
:discussion
|
||||
]
|
||||
|
||||
@conversation_preloads [
|
||||
@discussion_preloads [
|
||||
:last_comment,
|
||||
:comments,
|
||||
:creator,
|
||||
@@ -231,21 +232,11 @@ defmodule Mobilizon.Conversations do
|
||||
@doc """
|
||||
Returns the list of public comments for the actor.
|
||||
"""
|
||||
@spec list_public_comments_for_actor(Actor.t(), integer | nil, integer | nil) ::
|
||||
{:ok, [Comment.t()], integer}
|
||||
@spec list_public_comments_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_public_comments_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||
comments =
|
||||
actor_id
|
||||
|> public_comments_for_actor_query()
|
||||
|> Page.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
|
||||
count_comments =
|
||||
actor_id
|
||||
|> count_comments_query()
|
||||
|> Repo.one()
|
||||
|
||||
{:ok, comments, count_comments}
|
||||
actor_id
|
||||
|> public_comments_for_actor_query()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -263,10 +254,10 @@ defmodule Mobilizon.Conversations do
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec get_comments_for_conversation(integer, integer | nil, integer | nil) :: Page.t()
|
||||
def get_comments_for_conversation(conversation_id, page \\ nil, limit \\ nil) do
|
||||
@spec get_comments_for_discussion(integer, integer | nil, integer | nil) :: Page.t()
|
||||
def get_comments_for_discussion(discussion_id, page \\ nil, limit \\ nil) do
|
||||
Comment
|
||||
|> where([c], c.conversation_id == ^conversation_id)
|
||||
|> where([c], c.discussion_id == ^discussion_id)
|
||||
|> order_by(asc: :inserted_at)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
@@ -277,80 +268,114 @@ defmodule Mobilizon.Conversations do
|
||||
@spec count_local_comments :: integer
|
||||
def count_local_comments, do: Repo.one(count_local_comments_query())
|
||||
|
||||
def get_conversation(conversation_id) do
|
||||
Conversation
|
||||
|> Repo.get(conversation_id)
|
||||
|> Repo.preload(@conversation_preloads)
|
||||
def get_discussion(discussion_id) do
|
||||
Discussion
|
||||
|> Repo.get(discussion_id)
|
||||
|> Repo.preload(@discussion_preloads)
|
||||
end
|
||||
|
||||
@spec find_conversations_for_actor(integer, integer | nil, integer | nil) :: Page.t()
|
||||
def find_conversations_for_actor(actor_id, page \\ nil, limit \\ nil) do
|
||||
Conversation
|
||||
@spec get_discussion_by_url(String.t() | nil) :: Discussion.t() | nil
|
||||
def get_discussion_by_url(nil), do: nil
|
||||
|
||||
def get_discussion_by_url(discussion_url) do
|
||||
Discussion
|
||||
|> Repo.get_by(url: discussion_url)
|
||||
|> Repo.preload(@discussion_preloads)
|
||||
end
|
||||
|
||||
def get_discussion_by_slug(discussion_slug) do
|
||||
Discussion
|
||||
|> Repo.get_by(slug: discussion_slug)
|
||||
|> Repo.preload(@discussion_preloads)
|
||||
end
|
||||
|
||||
@spec find_discussions_for_actor(integer, integer | nil, integer | nil) :: Page.t()
|
||||
def find_discussions_for_actor(actor_id, page \\ nil, limit \\ nil) do
|
||||
Discussion
|
||||
|> where([c], c.actor_id == ^actor_id)
|
||||
|> preload(^@conversation_preloads)
|
||||
|> preload(^@discussion_preloads)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a conversation.
|
||||
Creates a discussion.
|
||||
"""
|
||||
@spec create_conversation(map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
|
||||
def create_conversation(attrs \\ %{}) do
|
||||
with {:ok, %{comment: %Comment{} = _comment, conversation: %Conversation{} = conversation}} <-
|
||||
@spec create_discussion(map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
|
||||
def create_discussion(attrs \\ %{}) do
|
||||
with {:ok, %{comment: %Comment{} = _comment, discussion: %Discussion{} = discussion}} <-
|
||||
Multi.new()
|
||||
|> Multi.insert(
|
||||
:comment,
|
||||
Comment.changeset(%Comment{}, Map.merge(attrs, %{actor_id: attrs.creator_id}))
|
||||
Comment.changeset(
|
||||
%Comment{},
|
||||
Map.merge(attrs, %{actor_id: attrs.creator_id, attributed_to_id: attrs.actor_id})
|
||||
)
|
||||
)
|
||||
|> Multi.insert(:conversation, fn %{comment: %Comment{id: comment_id}} ->
|
||||
Conversation.changeset(
|
||||
%Conversation{},
|
||||
|> Multi.insert(:discussion, fn %{comment: %Comment{id: comment_id}} ->
|
||||
Discussion.changeset(
|
||||
%Discussion{},
|
||||
Map.merge(attrs, %{last_comment_id: comment_id})
|
||||
)
|
||||
end)
|
||||
|> Multi.update(:comment_conversation, fn %{
|
||||
comment: %Comment{} = comment,
|
||||
conversation: %Conversation{
|
||||
id: conversation_id
|
||||
}
|
||||
} ->
|
||||
Changeset.change(comment, %{conversation_id: conversation_id})
|
||||
|> Multi.update(:comment_discussion, fn %{
|
||||
comment: %Comment{} = comment,
|
||||
discussion: %Discussion{
|
||||
id: discussion_id,
|
||||
url: discussion_url
|
||||
}
|
||||
} ->
|
||||
Changeset.change(comment, %{discussion_id: discussion_id, url: discussion_url})
|
||||
end)
|
||||
|> Repo.transaction() do
|
||||
{:ok, conversation}
|
||||
{:ok, discussion}
|
||||
end
|
||||
end
|
||||
|
||||
def reply_to_conversation(%Conversation{id: conversation_id} = conversation, attrs \\ %{}) do
|
||||
with {:ok, %{comment: %Comment{} = comment, conversation: %Conversation{} = conversation}} <-
|
||||
def reply_to_discussion(%Discussion{id: discussion_id} = discussion, attrs \\ %{}) do
|
||||
with {:ok, %{comment: %Comment{} = comment, discussion: %Discussion{} = discussion}} <-
|
||||
Multi.new()
|
||||
|> Multi.insert(
|
||||
:comment,
|
||||
Comment.changeset(%Comment{}, Map.merge(attrs, %{conversation_id: conversation_id}))
|
||||
Comment.changeset(
|
||||
%Comment{},
|
||||
Map.merge(attrs, %{
|
||||
discussion_id: discussion_id,
|
||||
actor_id: Map.get(attrs, :creator_id, attrs.actor_id)
|
||||
})
|
||||
)
|
||||
)
|
||||
|> Multi.update(:conversation, fn %{comment: %Comment{id: comment_id}} ->
|
||||
Conversation.changeset(
|
||||
conversation,
|
||||
|> Multi.update(:discussion, fn %{comment: %Comment{id: comment_id}} ->
|
||||
Discussion.changeset(
|
||||
discussion,
|
||||
%{last_comment_id: comment_id}
|
||||
)
|
||||
end)
|
||||
|> Repo.transaction() do
|
||||
# For some reason conversation is not updated
|
||||
{:ok, Map.put(conversation, :last_comment, comment)}
|
||||
# Discussion is not updated
|
||||
{:ok, Map.put(discussion, :last_comment, comment)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update a conversation. Only their title for now.
|
||||
Update a discussion. Only their title for now.
|
||||
"""
|
||||
@spec update_conversation(Conversation.t(), map()) ::
|
||||
{:ok, Conversation.t()} | {:error, Changeset.t()}
|
||||
def update_conversation(%Conversation{} = conversation, attrs \\ %{}) do
|
||||
conversation
|
||||
|> Conversation.changeset(attrs)
|
||||
@spec update_discussion(Discussion.t(), map()) ::
|
||||
{:ok, Discussion.t()} | {:error, Changeset.t()}
|
||||
def update_discussion(%Discussion{} = discussion, attrs \\ %{}) do
|
||||
discussion
|
||||
|> Discussion.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Delete a discussion.
|
||||
"""
|
||||
@spec delete_discussion(Discussion.t()) :: {:ok, Discussion.t()} | {:error, Changeset.t()}
|
||||
def delete_discussion(%Discussion{} = discussion) do
|
||||
discussion
|
||||
|> Repo.delete()
|
||||
end
|
||||
|
||||
defp public_comments_for_actor_query(actor_id) do
|
||||
Comment
|
||||
|> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility)
|
||||
@@ -365,11 +390,6 @@ defmodule Mobilizon.Conversations do
|
||||
|> preload_for_comment()
|
||||
end
|
||||
|
||||
@spec count_comments_query(integer) :: Ecto.Query.t()
|
||||
defp count_comments_query(actor_id) do
|
||||
from(c in Comment, select: count(c.id), where: c.actor_id == ^actor_id)
|
||||
end
|
||||
|
||||
@spec count_local_comments_query :: Ecto.Query.t()
|
||||
defp count_local_comments_query do
|
||||
from(
|
||||
@@ -382,6 +402,6 @@ defmodule Mobilizon.Conversations do
|
||||
@spec preload_for_comment(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
defp preload_for_comment(query), do: preload(query, ^@comment_preloads)
|
||||
|
||||
# @spec preload_for_conversation(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
# defp preload_for_conversation(query), do: preload(query, ^@conversation_preloads)
|
||||
# @spec preload_for_discussion(Ecto.Query.t()) :: Ecto.Query.t()
|
||||
# defp preload_for_discussion(query), do: preload(query, ^@discussion_preloads)
|
||||
end
|
||||
@@ -13,7 +13,7 @@ defmodule Mobilizon.Events.Event do
|
||||
alias Mobilizon.{Addresses, Events, Media, Mention}
|
||||
alias Mobilizon.Addresses.Address
|
||||
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
|
||||
alias Mobilizon.Events.{
|
||||
EventOptions,
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule Mobilizon.Events.EventOptions do
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Conversations.CommentModeration
|
||||
alias Mobilizon.Discussions.CommentModeration
|
||||
|
||||
alias Mobilizon.Events.{
|
||||
EventOffer,
|
||||
|
||||
@@ -380,24 +380,19 @@ defmodule Mobilizon.Events do
|
||||
@doc """
|
||||
Lists public events for the actor, with all associations loaded.
|
||||
"""
|
||||
@spec list_public_events_for_actor(Actor.t(), integer | nil, integer | nil) ::
|
||||
{:ok, [Event.t()], integer}
|
||||
def list_public_events_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||
events =
|
||||
actor_id
|
||||
|> event_for_actor_query()
|
||||
|> filter_public_visibility()
|
||||
|> filter_draft()
|
||||
|> preload_for_event()
|
||||
|> Page.paginate(page, limit)
|
||||
|> Repo.all()
|
||||
@spec list_public_events_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def list_public_events_for_actor(actor, page \\ nil, limit \\ nil)
|
||||
|
||||
events_count =
|
||||
actor_id
|
||||
|> count_events_for_actor_query()
|
||||
|> Repo.one()
|
||||
def list_public_events_for_actor(%Actor{type: :Group} = group, page, limit),
|
||||
do: list_organized_events_for_group(group, page, limit)
|
||||
|
||||
{:ok, events, events_count}
|
||||
def list_public_events_for_actor(%Actor{id: actor_id}, page, limit) do
|
||||
actor_id
|
||||
|> event_for_actor_query()
|
||||
|> filter_public_visibility()
|
||||
|> filter_draft()
|
||||
|> preload_for_event()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@spec list_organized_events_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
@@ -1321,15 +1316,6 @@ defmodule Mobilizon.Events do
|
||||
)
|
||||
end
|
||||
|
||||
@spec count_events_for_actor_query(integer | String.t()) :: Ecto.Query.t()
|
||||
defp count_events_for_actor_query(actor_id) do
|
||||
from(
|
||||
e in Event,
|
||||
select: count(e.id),
|
||||
where: e.organizer_actor_id == ^actor_id
|
||||
)
|
||||
end
|
||||
|
||||
@spec count_local_events_query :: Ecto.Query.t()
|
||||
defp count_local_events_query do
|
||||
from(e in Event, select: count(e.id), where: e.local == ^true)
|
||||
|
||||
@@ -19,7 +19,7 @@ defmodule Mobilizon.Events.Participant do
|
||||
url: String.t(),
|
||||
event: Event.t(),
|
||||
actor: Actor.t(),
|
||||
metadata: Map.t()
|
||||
metadata: map()
|
||||
}
|
||||
|
||||
@required_attrs [:url, :role, :event_id, :actor_id]
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule Mobilizon.Mention do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Storage.Repo
|
||||
|
||||
|
||||
137
lib/mobilizon/posts/post.ex
Normal file
137
lib/mobilizon/posts/post.ex
Normal file
@@ -0,0 +1,137 @@
|
||||
defmodule Mobilizon.Posts.Post.TitleSlug do
|
||||
@moduledoc """
|
||||
Module to generate the slug for posts
|
||||
"""
|
||||
use EctoAutoslugField.Slug, from: [:title, :id], to: :slug
|
||||
|
||||
def build_slug([title, id], %Ecto.Changeset{valid?: true}) do
|
||||
[title, ShortUUID.encode!(id)]
|
||||
|> Enum.join("-")
|
||||
|> Slugger.slugify()
|
||||
end
|
||||
|
||||
def build_slug(_sources, %Ecto.Changeset{valid?: false}), do: ""
|
||||
end
|
||||
|
||||
defmodule Mobilizon.Posts.Post do
|
||||
@moduledoc """
|
||||
Module that represent Posts published by groups
|
||||
"""
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
alias Ecto.Changeset
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Tag
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Posts.Post.TitleSlug
|
||||
alias Mobilizon.Posts.PostVisibility
|
||||
alias Mobilizon.Web.Endpoint
|
||||
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
url: String.t(),
|
||||
local: boolean,
|
||||
slug: String.t(),
|
||||
body: String.t(),
|
||||
title: String.t(),
|
||||
draft: boolean,
|
||||
visibility: PostVisibility.t(),
|
||||
publish_at: DateTime.t(),
|
||||
author: Actor.t(),
|
||||
attributed_to: Actor.t(),
|
||||
picture: Picture.t(),
|
||||
tags: [Tag.t()]
|
||||
}
|
||||
|
||||
@primary_key {:id, Ecto.UUID, autogenerate: true}
|
||||
|
||||
schema "posts" do
|
||||
field(:body, :string)
|
||||
field(:draft, :boolean, default: false)
|
||||
field(:local, :boolean, default: true)
|
||||
field(:slug, TitleSlug.Type)
|
||||
field(:title, :string)
|
||||
field(:url, :string)
|
||||
field(:publish_at, :utc_datetime)
|
||||
field(:visibility, PostVisibility, default_value: :public)
|
||||
belongs_to(:author, Actor)
|
||||
belongs_to(:attributed_to, Actor)
|
||||
belongs_to(:picture, Picture, on_replace: :update)
|
||||
many_to_many(:tags, Tag, join_through: "posts_tags", on_replace: :delete)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
@required_attrs [
|
||||
:id,
|
||||
:title,
|
||||
:body,
|
||||
:draft,
|
||||
:slug,
|
||||
:url,
|
||||
:author_id,
|
||||
:attributed_to_id
|
||||
]
|
||||
@optional_attrs [:picture_id, :local, :publish_at, :visibility]
|
||||
@attrs @required_attrs ++ @optional_attrs
|
||||
|
||||
@doc false
|
||||
def changeset(%__MODULE__{} = post, attrs) do
|
||||
post
|
||||
|> cast(attrs, @attrs)
|
||||
|> maybe_generate_id()
|
||||
|> put_tags(attrs)
|
||||
|> maybe_put_publish_date()
|
||||
# Validate ID and title here because they're needed for slug
|
||||
|> validate_required([:id, :title])
|
||||
|> TitleSlug.maybe_generate_slug()
|
||||
|> TitleSlug.unique_constraint()
|
||||
|> maybe_generate_url()
|
||||
|> validate_required(@required_attrs)
|
||||
end
|
||||
|
||||
defp maybe_generate_id(%Ecto.Changeset{} = changeset) do
|
||||
case fetch_field(changeset, :id) do
|
||||
res when res in [:error, {:data, nil}] ->
|
||||
put_change(changeset, :id, Ecto.UUID.generate())
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
@spec maybe_generate_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
defp maybe_generate_url(%Ecto.Changeset{} = changeset) do
|
||||
with res when res in [:error, {:data, nil}] <- fetch_field(changeset, :url),
|
||||
{changes, id_and_slug} when changes in [:changes, :data] <-
|
||||
fetch_field(changeset, :slug),
|
||||
url <- generate_url(id_and_slug) do
|
||||
put_change(changeset, :url, url)
|
||||
else
|
||||
_ -> changeset
|
||||
end
|
||||
end
|
||||
|
||||
@spec generate_url(String.t()) :: String.t()
|
||||
defp generate_url(id_and_slug), do: Routes.page_url(Endpoint, :post, id_and_slug)
|
||||
|
||||
@spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
|
||||
defp put_tags(changeset, %{"tags" => tags}),
|
||||
do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1))
|
||||
|
||||
defp put_tags(changeset, %{tags: tags}),
|
||||
do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1))
|
||||
|
||||
defp put_tags(changeset, _), do: changeset
|
||||
|
||||
defp process_tag(tag), do: Tag.changeset(%Tag{}, tag)
|
||||
|
||||
defp maybe_put_publish_date(%Changeset{} = changeset) do
|
||||
publish_at =
|
||||
if get_field(changeset, :draft, true) == false,
|
||||
do: DateTime.utc_now() |> DateTime.truncate(:second),
|
||||
else: nil
|
||||
|
||||
put_change(changeset, :publish_at, publish_at)
|
||||
end
|
||||
end
|
||||
135
lib/mobilizon/posts/posts.ex
Normal file
135
lib/mobilizon/posts/posts.ex
Normal file
@@ -0,0 +1,135 @@
|
||||
defmodule Mobilizon.Posts do
|
||||
@moduledoc """
|
||||
The Posts context.
|
||||
"""
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events.Tag
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Storage.{Page, Repo}
|
||||
|
||||
import Ecto.Query
|
||||
require Logger
|
||||
|
||||
@post_preloads [:author, :attributed_to, :picture]
|
||||
|
||||
import EctoEnum
|
||||
|
||||
defenum(PostVisibility, :post_visibility, [
|
||||
:public,
|
||||
:unlisted,
|
||||
:restricted,
|
||||
:private
|
||||
])
|
||||
|
||||
@doc """
|
||||
Returns the list of recent posts for a group
|
||||
"""
|
||||
@spec get_posts_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def get_posts_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
|
||||
group_id
|
||||
|> do_get_posts_for_group()
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@spec get_public_posts_for_group(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
||||
def get_public_posts_for_group(%Actor{id: group_id}, page \\ nil, limit \\ nil) do
|
||||
group_id
|
||||
|> do_get_posts_for_group()
|
||||
|> where([p], p.visibility == ^:public and not p.draft)
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
def do_get_posts_for_group(group_id) do
|
||||
Post
|
||||
|> where(attributed_to_id: ^group_id)
|
||||
|> order_by(desc: :inserted_at)
|
||||
|> preload([p], [:author, :attributed_to, :picture])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a post by it's ID
|
||||
"""
|
||||
@spec get_post(integer | String.t()) :: Post.t() | nil
|
||||
def get_post(nil), do: nil
|
||||
def get_post(id), do: Repo.get(Post, id)
|
||||
|
||||
@spec get_post_with_preloads(integer | String.t()) :: Post.t() | nil
|
||||
def get_post_with_preloads(id) do
|
||||
Post
|
||||
|> Repo.get(id)
|
||||
|> Repo.preload(@post_preloads)
|
||||
end
|
||||
|
||||
@spec get_post_by_slug(String.t()) :: Post.t() | nil
|
||||
def get_post_by_slug(nil), do: nil
|
||||
def get_post_by_slug(slug), do: Repo.get_by(Post, slug: slug)
|
||||
|
||||
@spec get_post_by_slug_with_preloads(String.t()) :: Post.t() | nil
|
||||
def get_post_by_slug_with_preloads(slug) do
|
||||
Post
|
||||
|> Repo.get_by(slug: slug)
|
||||
|> Repo.preload(@post_preloads)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get a post by it's URL
|
||||
"""
|
||||
@spec get_post_by_url(String.t()) :: Post.t() | nil
|
||||
def get_post_by_url(url), do: Repo.get_by(Post, url: url)
|
||||
|
||||
@spec get_post_by_url_with_preloads(String.t()) :: Post.t() | nil
|
||||
def get_post_by_url_with_preloads(url) do
|
||||
Post
|
||||
|> Repo.get_by(url: url)
|
||||
|> Repo.preload(@post_preloads)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a post.
|
||||
"""
|
||||
@spec create_post(map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
|
||||
def create_post(attrs \\ %{}) do
|
||||
%Post{}
|
||||
|> Post.changeset(attrs)
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a post.
|
||||
"""
|
||||
@spec update_post(Post.t(), map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
|
||||
def update_post(%Post{} = post, attrs) do
|
||||
post
|
||||
|> Repo.preload(:tags)
|
||||
|> Post.changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes a post
|
||||
"""
|
||||
@spec delete_post(Post.t()) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()}
|
||||
def delete_post(%Post{} = post), do: Repo.delete(post)
|
||||
|
||||
@doc """
|
||||
Returns the list of tags for the post.
|
||||
"""
|
||||
@spec list_tags_for_post(integer | String.t()) :: [Tag.t()]
|
||||
def list_tags_for_post(post_id) do
|
||||
{:ok, uuid} = Ecto.UUID.dump(post_id)
|
||||
|
||||
uuid
|
||||
|> tags_for_post_query()
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec tags_for_post_query(integer) :: Ecto.Query.t()
|
||||
defp tags_for_post_query(post_id) do
|
||||
from(
|
||||
t in Tag,
|
||||
join: p in "posts_tags",
|
||||
on: t.id == p.tag_id,
|
||||
where: p.post_id == ^post_id
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -8,7 +8,7 @@ defmodule Mobilizon.Reports.Report do
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Reports.{Note, ReportStatus}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ defmodule Mobilizon.Resources do
|
||||
Resource
|
||||
|> where(actor_id: ^group_id)
|
||||
|> order_by(desc: :updated_at)
|
||||
|> preload([r], [:actor, :creator])
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
@@ -55,6 +56,7 @@ defmodule Mobilizon.Resources do
|
||||
Resource
|
||||
|> where([r], r.parent_id == ^resource_id)
|
||||
|> order_by(asc: :type)
|
||||
|> preload([r], [:actor, :creator])
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ defmodule Mobilizon.Todos do
|
||||
TodoList
|
||||
|> where(actor_id: ^group_id)
|
||||
|> order_by(desc: :updated_at)
|
||||
|> preload([:actor])
|
||||
|> Page.build_page(page, limit)
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,15 @@ defmodule Mobilizon.Users.Setting do
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Users.{NotificationPendingNotificationDelay, User}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
timezone: String.t(),
|
||||
notification_on_day: boolean,
|
||||
notification_each_week: boolean,
|
||||
notification_before_event: boolean,
|
||||
notification_pending_participation: NotificationPendingNotificationDelay.t(),
|
||||
user: User.t()
|
||||
}
|
||||
|
||||
@required_attrs [:user_id]
|
||||
|
||||
@optional_attrs [
|
||||
|
||||
@@ -47,7 +47,7 @@ defmodule Mobilizon.Service.Export.Feed do
|
||||
defp fetch_actor_event_feed(name) do
|
||||
with %Actor{} = actor <- Actors.get_local_actor_by_name(name),
|
||||
{:visibility, true} <- {:visibility, Actor.is_public_visibility(actor)},
|
||||
{:ok, events, _count} <- Events.list_public_events_for_actor(actor) do
|
||||
%Page{elements: events} <- Events.list_public_events_for_actor(actor) do
|
||||
{:ok, build_actor_feed(actor, events)}
|
||||
else
|
||||
err ->
|
||||
|
||||
@@ -49,7 +49,8 @@ defmodule Mobilizon.Service.Export.ICalendar do
|
||||
@spec export_public_actor(Actor.t()) :: String.t()
|
||||
def export_public_actor(%Actor{} = actor) do
|
||||
with true <- Actor.is_public_visibility(actor),
|
||||
{:ok, events, _} <- Events.list_public_events_for_actor(actor) do
|
||||
%Page{elements: events} <-
|
||||
Events.list_public_events_for_actor(actor) do
|
||||
{:ok, %ICalendar{events: events |> Enum.map(&do_export_event/1)} |> ICalendar.to_ics()}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,8 +4,8 @@ defmodule Mobilizon.Service.Geospatial.Addok do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Service.Geospatial.Provider
|
||||
alias Mobilizon.Service.HTTP.BaseClient
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -15,26 +15,18 @@ defmodule Mobilizon.Service.Geospatial.Addok do
|
||||
@default_country Application.get_env(:mobilizon, __MODULE__) |> get_in([:default_country]) ||
|
||||
"France"
|
||||
|
||||
@http_options [
|
||||
follow_redirect: true,
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@impl Provider
|
||||
@doc """
|
||||
Addok implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||
"""
|
||||
@spec geocode(String.t(), keyword()) :: list(Address.t())
|
||||
def geocode(lon, lat, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
|
||||
|
||||
Logger.debug("Asking addok for addresses with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
%{"features" => features} <- body do
|
||||
process_data(features)
|
||||
else
|
||||
_ -> []
|
||||
@@ -47,14 +39,11 @@ defmodule Mobilizon.Service.Geospatial.Addok do
|
||||
"""
|
||||
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||
def search(q, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
url = build_url(:search, %{q: q}, options)
|
||||
Logger.debug("Asking addok for addresses with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
%{"features" => features} <- body do
|
||||
process_data(features)
|
||||
else
|
||||
_ -> []
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
|
||||
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Service.Geospatial.Provider
|
||||
alias Mobilizon.Service.HTTP.BaseClient
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -28,11 +29,6 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
|
||||
|
||||
@api_key_missing_message "API Key required to use Google Maps"
|
||||
|
||||
@http_options [
|
||||
follow_redirect: true,
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@impl Provider
|
||||
@doc """
|
||||
Google Maps implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||
@@ -43,12 +39,11 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
|
||||
|
||||
Logger.debug("Asking Google Maps for reverse geocode with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, [], @http_options),
|
||||
{:ok, %{"results" => results, "status" => "OK"}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
%{"results" => results, "status" => "OK"} <- body do
|
||||
Enum.map(results, fn entry -> process_data(entry, options) end)
|
||||
else
|
||||
{:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} ->
|
||||
%{"status" => "REQUEST_DENIED", "error_message" => error_message} ->
|
||||
raise ArgumentError, message: to_string(error_message)
|
||||
end
|
||||
end
|
||||
@@ -63,15 +58,14 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
|
||||
|
||||
Logger.debug("Asking Google Maps for addresses with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, [], @http_options),
|
||||
{:ok, %{"results" => results, "status" => "OK"}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
%{"results" => results, "status" => "OK"} <- body do
|
||||
results |> Enum.map(fn entry -> process_data(entry, options) end)
|
||||
else
|
||||
{:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} ->
|
||||
%{"status" => "REQUEST_DENIED", "error_message" => error_message} ->
|
||||
raise ArgumentError, message: to_string(error_message)
|
||||
|
||||
{:ok, %{"results" => [], "status" => "ZERO_RESULTS"}} ->
|
||||
%{"results" => [], "status" => "ZERO_RESULTS"} ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
@@ -165,18 +159,17 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
|
||||
|
||||
Logger.debug("Asking Google Maps for details with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, [], @http_options),
|
||||
{:ok, %{"result" => %{"name" => name}, "status" => "OK"}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
%{"result" => %{"name" => name}, "status" => "OK"} <- body do
|
||||
name
|
||||
else
|
||||
{:ok, %{"status" => "REQUEST_DENIED", "error_message" => error_message}} ->
|
||||
%{"status" => "REQUEST_DENIED", "error_message" => error_message} ->
|
||||
raise ArgumentError, message: to_string(error_message)
|
||||
|
||||
{:ok, %{"status" => "INVALID_REQUEST"}} ->
|
||||
%{"status" => "INVALID_REQUEST"} ->
|
||||
raise ArgumentError, message: "Invalid Request"
|
||||
|
||||
{:ok, %{"results" => [], "status" => "ZERO_RESULTS"}} ->
|
||||
%{"results" => [], "status" => "ZERO_RESULTS"} ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,8 +10,8 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Service.Geospatial.Provider
|
||||
alias Mobilizon.Service.HTTP.BaseClient
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -21,11 +21,6 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
|
||||
|
||||
@api_key_missing_message "API Key required to use MapQuest"
|
||||
|
||||
@http_options [
|
||||
follow_redirect: true,
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@impl Provider
|
||||
@doc """
|
||||
MapQuest implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||
@@ -35,25 +30,21 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
|
||||
api_key = Keyword.get(options, :api_key, @api_key)
|
||||
limit = Keyword.get(options, :limit, 10)
|
||||
open_data = Keyword.get(options, :open_data, true)
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
|
||||
prefix = if open_data, do: "open", else: "www"
|
||||
|
||||
if is_nil(api_key), do: raise(ArgumentError, message: @api_key_missing_message)
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(
|
||||
with {:ok, %{status: 200, body: body}} <-
|
||||
BaseClient.get(
|
||||
"https://#{prefix}.mapquestapi.com/geocoding/v1/reverse?key=#{api_key}&location=#{
|
||||
lat
|
||||
},#{lon}&maxResults=#{limit}",
|
||||
headers,
|
||||
@http_options
|
||||
},#{lon}&maxResults=#{limit}"
|
||||
),
|
||||
{:ok, %{"results" => results, "info" => %{"statuscode" => 0}}} <- Poison.decode(body) do
|
||||
%{"results" => results, "info" => %{"statuscode" => 0}} <- body do
|
||||
results |> Enum.map(&process_data/1)
|
||||
else
|
||||
{:ok, %HTTPoison.Response{status_code: 403, body: err}} ->
|
||||
{:ok, %{status: 403, body: err}} ->
|
||||
raise(ArgumentError, message: err)
|
||||
end
|
||||
end
|
||||
@@ -64,8 +55,6 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
|
||||
"""
|
||||
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||
def search(q, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
limit = Keyword.get(options, :limit, 10)
|
||||
api_key = Keyword.get(options, :api_key, @api_key)
|
||||
|
||||
@@ -82,12 +71,11 @@ defmodule Mobilizon.Service.Geospatial.MapQuest do
|
||||
|
||||
Logger.debug("Asking MapQuest for addresses with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"results" => results, "info" => %{"statuscode" => 0}}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
%{"results" => results, "info" => %{"statuscode" => 0}} <- body do
|
||||
results |> Enum.map(&process_data/1)
|
||||
else
|
||||
{:ok, %HTTPoison.Response{status_code: 403, body: err}} ->
|
||||
{:ok, %{status: 403, body: err}} ->
|
||||
raise(ArgumentError, message: err)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,8 +8,8 @@ defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Service.Geospatial.Provider
|
||||
alias Mobilizon.Service.HTTP.BaseClient
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -17,25 +17,17 @@ defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do
|
||||
|
||||
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
|
||||
|
||||
@http_options [
|
||||
follow_redirect: true,
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@impl Provider
|
||||
@doc """
|
||||
Mimirsbrunn implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||
"""
|
||||
@spec geocode(number(), number(), keyword()) :: list(Address.t())
|
||||
def geocode(lon, lat, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
|
||||
Logger.debug("Asking Mimirsbrunn for reverse geocoding with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
{:ok, %{"features" => features}} <- Jason.decode(body) do
|
||||
process_data(features)
|
||||
else
|
||||
_ -> []
|
||||
@@ -48,14 +40,11 @@ defmodule Mobilizon.Service.Geospatial.Mimirsbrunn do
|
||||
"""
|
||||
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||
def search(q, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
url = build_url(:search, %{q: q}, options)
|
||||
Logger.debug("Asking Mimirsbrunn for addresses with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
{:ok, %{"features" => features}} <- Jason.decode(body) do
|
||||
process_data(features)
|
||||
else
|
||||
_ -> []
|
||||
|
||||
@@ -4,8 +4,8 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Service.Geospatial.Provider
|
||||
alias Mobilizon.Service.HTTP.BaseClient
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -14,25 +14,17 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
|
||||
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
|
||||
@api_key Application.get_env(:mobilizon, __MODULE__) |> get_in([:api_key])
|
||||
|
||||
@http_options [
|
||||
follow_redirect: true,
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@impl Provider
|
||||
@doc """
|
||||
Nominatim implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||
"""
|
||||
@spec geocode(String.t(), keyword()) :: list(Address.t())
|
||||
def geocode(lon, lat, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
|
||||
Logger.debug("Asking Nominatim for geocode with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
%{"features" => features} <- body do
|
||||
features |> process_data() |> Enum.filter(& &1)
|
||||
else
|
||||
_ -> []
|
||||
@@ -45,14 +37,11 @@ defmodule Mobilizon.Service.Geospatial.Nominatim do
|
||||
"""
|
||||
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||
def search(q, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
url = build_url(:search, %{q: q}, options)
|
||||
Logger.debug("Asking Nominatim for addresses with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
%{"features" => features} <- body do
|
||||
features |> process_data() |> Enum.filter(& &1)
|
||||
else
|
||||
_ -> []
|
||||
|
||||
@@ -6,8 +6,8 @@ defmodule Mobilizon.Service.Geospatial.Pelias do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Service.Geospatial.Provider
|
||||
alias Mobilizon.Service.HTTP.BaseClient
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -15,25 +15,17 @@ defmodule Mobilizon.Service.Geospatial.Pelias do
|
||||
|
||||
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
|
||||
|
||||
@http_options [
|
||||
follow_redirect: true,
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@impl Provider
|
||||
@doc """
|
||||
Pelias implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||
"""
|
||||
@spec geocode(number(), number(), keyword()) :: list(Address.t())
|
||||
def geocode(lon, lat, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
|
||||
Logger.debug("Asking Pelias for reverse geocoding with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
{:ok, %{"features" => features}} <- Jason.decode(body) do
|
||||
process_data(features)
|
||||
else
|
||||
_ -> []
|
||||
@@ -46,14 +38,11 @@ defmodule Mobilizon.Service.Geospatial.Pelias do
|
||||
"""
|
||||
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||
def search(q, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
url = build_url(:search, %{q: q}, options)
|
||||
Logger.debug("Asking Pelias for addresses with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
{:ok, %{"features" => features}} <- Jason.decode(body) do
|
||||
process_data(features)
|
||||
else
|
||||
_ -> []
|
||||
|
||||
@@ -4,8 +4,8 @@ defmodule Mobilizon.Service.Geospatial.Photon do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Service.Geospatial.Provider
|
||||
alias Mobilizon.Service.HTTP.BaseClient
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -13,11 +13,6 @@ defmodule Mobilizon.Service.Geospatial.Photon do
|
||||
|
||||
@endpoint Application.get_env(:mobilizon, __MODULE__) |> get_in([:endpoint])
|
||||
|
||||
@http_options [
|
||||
follow_redirect: true,
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@impl Provider
|
||||
@doc """
|
||||
Photon implementation for `c:Mobilizon.Service.Geospatial.Provider.geocode/3`.
|
||||
@@ -26,14 +21,11 @@ defmodule Mobilizon.Service.Geospatial.Photon do
|
||||
"""
|
||||
@spec geocode(number(), number(), keyword()) :: list(Address.t())
|
||||
def geocode(lon, lat, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
url = build_url(:geocode, %{lon: lon, lat: lat}, options)
|
||||
Logger.debug("Asking photon for reverse geocoding with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
%{"features" => features} <- body do
|
||||
process_data(features)
|
||||
else
|
||||
_ -> []
|
||||
@@ -46,14 +38,11 @@ defmodule Mobilizon.Service.Geospatial.Photon do
|
||||
"""
|
||||
@spec search(String.t(), keyword()) :: list(Address.t())
|
||||
def search(q, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
url = build_url(:search, %{q: q}, options)
|
||||
Logger.debug("Asking photon for addresses with #{url}")
|
||||
|
||||
with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <-
|
||||
HTTPoison.get(url, headers, @http_options),
|
||||
{:ok, %{"features" => features}} <- Poison.decode(body) do
|
||||
with {:ok, %{status: 200, body: body}} <- BaseClient.get(url),
|
||||
%{"features" => features} <- body do
|
||||
process_data(features)
|
||||
else
|
||||
_ -> []
|
||||
|
||||
@@ -15,7 +15,6 @@ defmodule Mobilizon.Service.Geospatial.Provider do
|
||||
|
||||
## Shared options
|
||||
|
||||
* `:user_agent` User-Agent string to send to the backend. Defaults to `"Mobilizon"` or `Mobilizon.Config.instance_user_agent/0`
|
||||
* `:lang` Lang in which to prefer results. Used as a request parameter or
|
||||
through an `Accept-Language` HTTP header. Defaults to `"en"`.
|
||||
* `:country_code` An ISO 3166 country code. String or `nil`
|
||||
|
||||
38
lib/service/http/activity_pub.ex
Normal file
38
lib/service/http/activity_pub.ex
Normal file
@@ -0,0 +1,38 @@
|
||||
defmodule Mobilizon.Service.HTTP.ActivityPub do
|
||||
@moduledoc """
|
||||
Tesla HTTP Client that is preconfigured to get and post ActivityPub content
|
||||
"""
|
||||
|
||||
alias Mobilizon.Config
|
||||
|
||||
@adapter Application.get_env(:tesla, __MODULE__, [])[:adapter] || Tesla.Adapter.Hackney
|
||||
@default_opts [
|
||||
recv_timeout: 20_000
|
||||
]
|
||||
@user_agent Config.instance_user_agent()
|
||||
|
||||
def client(options \\ []) do
|
||||
headers = Keyword.get(options, :headers, [])
|
||||
opts = Keyword.merge(@default_opts, Keyword.get(options, :opts, []))
|
||||
|
||||
middleware = [
|
||||
{Tesla.Middleware.Headers,
|
||||
[{"User-Agent", @user_agent}, {"Accept", "application/activity+json"}] ++ headers},
|
||||
Tesla.Middleware.FollowRedirects,
|
||||
{Tesla.Middleware.Timeout, timeout: 10_000},
|
||||
{Tesla.Middleware.JSON, decode_content_types: "application/activity+json"}
|
||||
]
|
||||
|
||||
adapter = {@adapter, opts}
|
||||
|
||||
Tesla.client(middleware, adapter)
|
||||
end
|
||||
|
||||
def get(client, url) do
|
||||
Tesla.get(client, url)
|
||||
end
|
||||
|
||||
def post(client, url, data) do
|
||||
Tesla.post(client, url, data)
|
||||
end
|
||||
end
|
||||
30
lib/service/http/base_client.ex
Normal file
30
lib/service/http/base_client.ex
Normal file
@@ -0,0 +1,30 @@
|
||||
defmodule Mobilizon.Service.HTTP.BaseClient do
|
||||
@moduledoc """
|
||||
Tesla HTTP Basic Client
|
||||
"""
|
||||
|
||||
use Tesla
|
||||
alias Mobilizon.Config
|
||||
|
||||
@default_opts [
|
||||
recv_timeout: 20_000
|
||||
]
|
||||
|
||||
adapter(Tesla.Adapter.Hackney, @default_opts)
|
||||
|
||||
@user_agent Config.instance_user_agent()
|
||||
|
||||
plug(Tesla.Middleware.FollowRedirects)
|
||||
|
||||
plug(Tesla.Middleware.Timeout, timeout: 10_000)
|
||||
|
||||
plug(Tesla.Middleware.Headers, [{"User-Agent", @user_agent}])
|
||||
|
||||
def get(url) do
|
||||
get(url)
|
||||
end
|
||||
|
||||
def post(url, data) do
|
||||
post(url, data)
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,6 @@
|
||||
defimpl Mobilizon.Service.Metadata, for: Mobilizon.Conversations.Comment do
|
||||
defimpl Mobilizon.Service.Metadata, for: Mobilizon.Discussions.Comment do
|
||||
alias Phoenix.HTML.Tag
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.Comment
|
||||
|
||||
def build_tags(%Comment{} = comment, _locale \\ "en") do
|
||||
[
|
||||
|
||||
@@ -2,10 +2,9 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
|
||||
alias Phoenix.HTML
|
||||
alias Phoenix.HTML.Tag
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Service.Formatter.HTML, as: HTMLFormatter
|
||||
alias Mobilizon.Web.JsonLD.ObjectView
|
||||
alias Mobilizon.Web.MediaProxy
|
||||
import Mobilizon.Web.Gettext
|
||||
import Mobilizon.Service.Metadata.Utils, only: [process_description: 2, strip_tags: 1]
|
||||
|
||||
def build_tags(%Event{} = event, locale \\ "en") do
|
||||
event = Map.put(event, :description, process_description(event.description, locale))
|
||||
@@ -41,24 +40,10 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
|
||||
]
|
||||
end
|
||||
|
||||
defp process_description(nil, locale), do: process_description("", locale)
|
||||
|
||||
defp process_description("", locale) do
|
||||
Gettext.put_locale(locale)
|
||||
gettext("The event organizer didn't add any description.")
|
||||
end
|
||||
|
||||
defp process_description(description, _locale) do
|
||||
description
|
||||
|> HTMLFormatter.strip_tags()
|
||||
|> String.slice(0..200)
|
||||
|> (&"#{&1}…").()
|
||||
end
|
||||
|
||||
# Insert JSON-LD schema by hand because Tag.content_tag wants to escape it
|
||||
defp json(%Event{title: title} = event) do
|
||||
"event.json"
|
||||
|> ObjectView.render(%{event: %{event | title: HTMLFormatter.strip_tags(title)}})
|
||||
|> ObjectView.render(%{event: %{event | title: strip_tags(title)}})
|
||||
|> Jason.encode!()
|
||||
end
|
||||
end
|
||||
|
||||
34
lib/service/metadata/post.ex
Normal file
34
lib/service/metadata/post.ex
Normal file
@@ -0,0 +1,34 @@
|
||||
defimpl Mobilizon.Service.Metadata, for: Mobilizon.Posts.Post do
|
||||
alias Phoenix.HTML
|
||||
alias Phoenix.HTML.Tag
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Web.JsonLD.ObjectView
|
||||
import Mobilizon.Service.Metadata.Utils, only: [process_description: 2, strip_tags: 1]
|
||||
|
||||
def build_tags(%Post{} = post, locale \\ "en") do
|
||||
post = Map.put(post, :body, process_description(post.body, locale))
|
||||
|
||||
tags = [
|
||||
Tag.tag(:meta, property: "og:title", content: post.title),
|
||||
Tag.tag(:meta, property: "og:url", content: post.url),
|
||||
Tag.tag(:meta, property: "og:description", content: post.body),
|
||||
Tag.tag(:meta, property: "og:type", content: "article"),
|
||||
Tag.tag(:meta, property: "twitter:card", content: "summary"),
|
||||
# Tell Search Engines what's the origin
|
||||
Tag.tag(:link, rel: "canonical", href: post.url)
|
||||
]
|
||||
|
||||
tags ++
|
||||
[
|
||||
Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"),
|
||||
~s{<script type="application/ld+json">#{json(post)}</script>} |> HTML.raw()
|
||||
]
|
||||
end
|
||||
|
||||
# Insert JSON-LD schema by hand because Tag.content_tag wants to escape it
|
||||
defp json(%Post{title: title} = post) do
|
||||
"post.json"
|
||||
|> ObjectView.render(%{post: %{post | title: strip_tags(title)}})
|
||||
|> Jason.encode!()
|
||||
end
|
||||
end
|
||||
@@ -3,10 +3,34 @@ defmodule Mobilizon.Service.Metadata.Utils do
|
||||
Tools to convert tags to string.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Service.Formatter.HTML, as: HTMLFormatter
|
||||
alias Phoenix.HTML
|
||||
import Mobilizon.Web.Gettext
|
||||
|
||||
@slice_limit 200
|
||||
|
||||
@spec stringify_tags(Enum.t()) :: String.t()
|
||||
def stringify_tags(tags), do: Enum.reduce(tags, "", &stringify_tag/2)
|
||||
|
||||
defp stringify_tag(tag, acc) when is_tuple(tag), do: acc <> HTML.safe_to_string(tag)
|
||||
defp stringify_tag(tag, acc) when is_binary(tag), do: acc <> tag
|
||||
|
||||
@spec strip_tags(String.t()) :: String.t()
|
||||
def strip_tags(text), do: HTMLFormatter.strip_tags(text)
|
||||
|
||||
@spec process_description(String.t(), String.t(), integer()) :: String.t()
|
||||
def process_description(description, locale \\ "en", limit \\ @slice_limit)
|
||||
def process_description(nil, locale, limit), do: process_description("", locale, limit)
|
||||
|
||||
def process_description("", locale, _limit) do
|
||||
Gettext.put_locale(locale)
|
||||
gettext("The event organizer didn't add any description.")
|
||||
end
|
||||
|
||||
def process_description(description, _locale, limit) do
|
||||
description
|
||||
|> HTMLFormatter.strip_tags()
|
||||
|> String.slice(0..limit)
|
||||
|> (&"#{&1}…").()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -16,24 +16,24 @@ defmodule Mobilizon.Service.RichMedia.Favicon do
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@spec fetch(String.t(), List.t()) :: {:ok, String.t()} | {:error, any()}
|
||||
@spec fetch(String.t(), Enum.t()) :: {:ok, String.t()} | {:error, any()}
|
||||
def fetch(url, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
|
||||
case HTTPoison.get(url, headers, @options) do
|
||||
{:ok, %HTTPoison.Response{status_code: code, body: body}} when code in 200..299 ->
|
||||
case Tesla.get(url, headers: headers, opts: @options) do
|
||||
{:ok, %{status: code, body: body}} when code in 200..299 ->
|
||||
find_favicon_url(url, body, headers)
|
||||
|
||||
{:ok, %HTTPoison.Response{}} ->
|
||||
{:ok, %{}} ->
|
||||
{:error, "Error while fetching the page"}
|
||||
|
||||
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||
{:error, reason}
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
@spec find_favicon_url(String.t(), String.t(), List.t()) :: {:ok, String.t()} | {:error, any()}
|
||||
@spec find_favicon_url(String.t(), String.t(), Enum.t()) :: {:ok, String.t()} | {:error, any()}
|
||||
defp find_favicon_url(url, body, headers) do
|
||||
Logger.debug("finding favicon URL for #{url}")
|
||||
|
||||
@@ -85,20 +85,20 @@ defmodule Mobilizon.Service.RichMedia.Favicon do
|
||||
end
|
||||
end
|
||||
|
||||
@spec find_favicon_in_root(String.t(), List.t()) :: {:ok, String.t()} | {:error, any()}
|
||||
@spec find_favicon_in_root(String.t(), Enum.t()) :: {:ok, String.t()} | {:error, any()}
|
||||
defp find_favicon_in_root(url, headers) do
|
||||
uri = URI.parse(url)
|
||||
favicon_url = "#{uri.scheme}://#{uri.host}/favicon.ico"
|
||||
|
||||
case HTTPoison.head(favicon_url, headers, @options) do
|
||||
{:ok, %HTTPoison.Response{status_code: code}} when code in 200..299 ->
|
||||
case Tesla.head(favicon_url, headers: headers, opts: @options) do
|
||||
{:ok, %{status: code}} when code in 200..299 ->
|
||||
{:ok, favicon_url}
|
||||
|
||||
{:ok, %HTTPoison.Response{}} ->
|
||||
{:ok, %{}} ->
|
||||
{:error, "Error while doing a HEAD request on the favicon"}
|
||||
|
||||
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||
{:error, reason}
|
||||
{:error, err} ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,7 +12,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
|
||||
timeout: 10_000,
|
||||
recv_timeout: 20_000,
|
||||
follow_redirect: true,
|
||||
# TODO: Remove me once Hackney/HTTPoison fixes their shit with TLS1.3 and OTP 23
|
||||
# TODO: Remove me once Hackney/HTTPoison fixes their issue with TLS1.3 and OTP 23
|
||||
ssl: [{:versions, [:"tlsv1.2"]}]
|
||||
]
|
||||
|
||||
@@ -46,7 +46,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
|
||||
{:error, "Cachex error: #{inspect(e)}"}
|
||||
end
|
||||
|
||||
@spec parse_url(String.t(), List.t()) :: {:ok, map()} | {:error, any()}
|
||||
@spec parse_url(String.t(), Enum.t()) :: {:ok, map()} | {:error, any()}
|
||||
defp parse_url(url, options \\ []) do
|
||||
user_agent = Keyword.get(options, :user_agent, Config.instance_user_agent())
|
||||
headers = [{"User-Agent", user_agent}]
|
||||
@@ -54,12 +54,12 @@ defmodule Mobilizon.Service.RichMedia.Parser do
|
||||
|
||||
try do
|
||||
with {:ok, _} <- prevent_local_address(url),
|
||||
{:ok, %HTTPoison.Response{body: body, status_code: code, headers: response_headers}}
|
||||
{:ok, %{body: body, status: code, headers: response_headers}}
|
||||
when code in 200..299 <-
|
||||
HTTPoison.get(
|
||||
Tesla.get(
|
||||
url,
|
||||
headers,
|
||||
@options
|
||||
headers: headers,
|
||||
opts: @options
|
||||
),
|
||||
{:is_html, _response_headers, true} <-
|
||||
{:is_html, response_headers, is_html(response_headers)} do
|
||||
@@ -87,7 +87,7 @@ defmodule Mobilizon.Service.RichMedia.Parser do
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_data_for_media(List.t(), String.t()) :: map()
|
||||
@spec get_data_for_media(Enum.t(), String.t()) :: map()
|
||||
defp get_data_for_media(response_headers, url) do
|
||||
data = %{title: get_filename_from_headers(response_headers) || get_filename_from_url(url)}
|
||||
|
||||
@@ -98,21 +98,21 @@ defmodule Mobilizon.Service.RichMedia.Parser do
|
||||
end
|
||||
end
|
||||
|
||||
@spec is_html(List.t()) :: boolean
|
||||
defp is_html(headers) do
|
||||
@spec is_html(Enum.t()) :: boolean
|
||||
def is_html(headers) do
|
||||
headers
|
||||
|> get_header("Content-Type")
|
||||
|> content_type_header_matches(["text/html", "application/xhtml"])
|
||||
end
|
||||
|
||||
@spec is_image(List.t()) :: boolean
|
||||
@spec is_image(Enum.t()) :: boolean
|
||||
defp is_image(headers) do
|
||||
headers
|
||||
|> get_header("Content-Type")
|
||||
|> content_type_header_matches(["image/"])
|
||||
end
|
||||
|
||||
@spec content_type_header_matches(String.t() | nil, List.t()) :: boolean
|
||||
@spec content_type_header_matches(String.t() | nil, Enum.t()) :: boolean
|
||||
defp content_type_header_matches(header, content_types)
|
||||
defp content_type_header_matches(nil, _content_types), do: false
|
||||
|
||||
@@ -120,15 +120,17 @@ defmodule Mobilizon.Service.RichMedia.Parser do
|
||||
Enum.any?(content_types, fn content_type -> String.starts_with?(header, content_type) end)
|
||||
end
|
||||
|
||||
@spec get_header(List.t(), String.t()) :: String.t() | nil
|
||||
@spec get_header(Enum.t(), String.t()) :: String.t() | nil
|
||||
defp get_header(headers, key) do
|
||||
key = String.downcase(key)
|
||||
|
||||
case List.keyfind(headers, key, 0) do
|
||||
{^key, value} -> String.downcase(value)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_filename_from_headers(List.t()) :: String.t() | nil
|
||||
@spec get_filename_from_headers(Enum.t()) :: String.t() | nil
|
||||
defp get_filename_from_headers(headers) do
|
||||
case get_header(headers, "Content-Disposition") do
|
||||
nil -> nil
|
||||
@@ -138,12 +140,16 @@ defmodule Mobilizon.Service.RichMedia.Parser do
|
||||
|
||||
@spec get_filename_from_url(String.t()) :: String.t()
|
||||
defp get_filename_from_url(url) do
|
||||
%URI{path: path} = URI.parse(url)
|
||||
case URI.parse(url) do
|
||||
%URI{path: nil} ->
|
||||
nil
|
||||
|
||||
path
|
||||
|> String.split("/", trim: true)
|
||||
|> Enum.at(-1)
|
||||
|> URI.decode()
|
||||
%URI{path: path} ->
|
||||
path
|
||||
|> String.split("/", trim: true)
|
||||
|> Enum.at(-1)
|
||||
|> URI.decode()
|
||||
end
|
||||
end
|
||||
|
||||
# The following is taken from https://github.com/elixir-plug/plug/blob/65986ad32f9aaae3be50dc80cbdd19b326578da7/lib/plug/parsers/multipart.ex#L207
|
||||
|
||||
@@ -42,7 +42,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OEmbed do
|
||||
end
|
||||
|
||||
defp get_oembed_data(url) do
|
||||
with {:ok, %HTTPoison.Response{body: json}} <- HTTPoison.get(url, [], @http_options),
|
||||
with {:ok, %{body: json}} <- Tesla.get(url, opts: @http_options),
|
||||
{:ok, data} <- Jason.decode(json),
|
||||
data <- data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end) do
|
||||
{:ok, data}
|
||||
|
||||
@@ -34,7 +34,7 @@ defmodule Mobilizon.Service.RichMedia.Parsers.OGP do
|
||||
|> Map.put(:height, get_integer_value(data, :"image:height"))
|
||||
end
|
||||
|
||||
@spec get_integer_value(Map.t(), atom()) :: integer() | nil
|
||||
@spec get_integer_value(map(), atom()) :: integer() | nil
|
||||
defp get_integer_value(data, key) do
|
||||
with value when not is_nil(value) <- Map.get(data, key),
|
||||
{value, ""} <- Integer.parse(value) do
|
||||
|
||||
@@ -3,7 +3,7 @@ defmodule Mobilizon.Service.Statistics do
|
||||
A module that provides cached statistics
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Conversations, Events, Users}
|
||||
alias Mobilizon.{Discussions, Events, Users}
|
||||
|
||||
def get_cached_value(key) do
|
||||
case Cachex.fetch(:statistics, key, fn key ->
|
||||
@@ -26,6 +26,6 @@ defmodule Mobilizon.Service.Statistics do
|
||||
end
|
||||
|
||||
defp create_cache(:local_comments) do
|
||||
Conversations.count_local_comments()
|
||||
Discussions.count_local_comments()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -98,7 +98,7 @@ defmodule Mobilizon.Service.Workers.Notification do
|
||||
else
|
||||
err ->
|
||||
require Logger
|
||||
Logger.error(inspect(err))
|
||||
Logger.debug(inspect(err))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
41
lib/web/cache/activity_pub.ex
vendored
41
lib/web/cache/activity_pub.ex
vendored
@@ -3,11 +3,12 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
|
||||
ActivityPub related cache.
|
||||
"""
|
||||
|
||||
alias Mobilizon.{Actors, Conversations, Events, Resources, Todos, Tombstone}
|
||||
alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos, Tombstone}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Conversations.Comment
|
||||
alias Mobilizon.Discussions.{Comment, Discussion}
|
||||
alias Mobilizon.Events.Event
|
||||
alias Mobilizon.Federation.ActivityPub.Relay
|
||||
alias Mobilizon.Posts.Post
|
||||
alias Mobilizon.Resources.Resource
|
||||
alias Mobilizon.Todos.{Todo, TodoList}
|
||||
alias Mobilizon.Web.Endpoint
|
||||
@@ -61,7 +62,7 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
|
||||
{:commit, Comment.t()} | {:ignore, nil}
|
||||
def get_comment_by_uuid_with_preload(uuid) do
|
||||
Cachex.fetch(@cache, "comment_" <> uuid, fn "comment_" <> uuid ->
|
||||
case Conversations.get_comment_from_uuid_with_preload(uuid) do
|
||||
case Discussions.get_comment_from_uuid_with_preload(uuid) do
|
||||
%Comment{} = comment ->
|
||||
{:commit, comment}
|
||||
|
||||
@@ -88,6 +89,40 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a post by its slug, with all associations loaded.
|
||||
"""
|
||||
@spec get_post_by_slug_with_preload(String.t()) ::
|
||||
{:commit, Post.t()} | {:ignore, nil}
|
||||
def get_post_by_slug_with_preload(slug) do
|
||||
Cachex.fetch(@cache, "post_" <> slug, fn "post_" <> slug ->
|
||||
case Posts.get_post_by_slug_with_preloads(slug) do
|
||||
%Post{} = post ->
|
||||
{:commit, post}
|
||||
|
||||
nil ->
|
||||
{:ignore, nil}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a discussion by its slug, with all associations loaded.
|
||||
"""
|
||||
@spec get_discussion_by_slug_with_preload(String.t()) ::
|
||||
{:commit, Discussion.t()} | {:ignore, nil}
|
||||
def get_discussion_by_slug_with_preload(slug) do
|
||||
Cachex.fetch(@cache, "discussion_" <> slug, fn "discussion_" <> slug ->
|
||||
case Discussions.get_discussion_by_slug(slug) do
|
||||
%Discussion{} = discussion ->
|
||||
{:commit, discussion}
|
||||
|
||||
nil ->
|
||||
{:ignore, nil}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a todo list by its UUID, with all associations loaded.
|
||||
"""
|
||||
|
||||
2
lib/web/cache/cache.ex
vendored
2
lib/web/cache/cache.ex
vendored
@@ -23,5 +23,7 @@ defmodule Mobilizon.Web.Cache do
|
||||
defdelegate get_resource_by_uuid_with_preload(uuid), to: ActivityPub
|
||||
defdelegate get_todo_list_by_uuid_with_preload(uuid), to: ActivityPub
|
||||
defdelegate get_todo_by_uuid_with_preload(uuid), to: ActivityPub
|
||||
defdelegate get_post_by_slug_with_preload(slug), to: ActivityPub
|
||||
defdelegate get_discussion_by_slug_with_preload(slug), to: ActivityPub
|
||||
defdelegate get_relay, to: ActivityPub
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user