Refactor Core things, including Ecto handling, ActivityPub & Transmogrifier modules

* Data doesn't need anymore to be converted to ActivityStream format to
be saved (this was taken from Pleroma and not at all a good idea here)
* Everything saved when creating an event is inserted into PostgreSQL in
a single transaction
This commit is contained in:
Thomas Citharel
2019-10-25 17:43:37 +02:00
parent 814cfbc8eb
commit cc820d6b63
69 changed files with 1881 additions and 1424 deletions

View File

@@ -0,0 +1,67 @@
defmodule Mix.Tasks.Mobilizon.MoveParticipantStats do
@moduledoc """
Temporary task to move participant stats in the events table
This task will be removed in version 1.0.0-beta.3
"""
use Mix.Task
alias Mobilizon.Storage.Repo
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Events.ParticipantRole
import Ecto.Query
require Logger
@shortdoc "Move participant stats to events table"
def run([]) do
Mix.Task.run("app.start")
events =
Event
|> preload([e], :tags)
|> Repo.all()
nb_events = length(events)
IO.puts(
"\nStarting inserting participants stats into #{nb_events} events, this can take a while…\n"
)
insert_participants_stats_into_events(events, nb_events)
end
defp insert_participants_stats_into_events([%Event{url: url} = event | events], nb_events) do
with roles <- ParticipantRole.__enum_map__(),
counts <-
Enum.reduce(roles, %{}, fn role, acc ->
Map.put(acc, role, count_participants(event, role))
end),
{:ok, _} <-
Events.update_event(event, %{
participant_stats: counts
}) do
Logger.debug("Added participants stats to event #{url}")
else
{:error, res} ->
Logger.error("Error while adding participants stats to event #{url} : #{inspect(res)}")
end
ProgressBar.render(nb_events - length(events), nb_events)
insert_participants_stats_into_events(events, nb_events)
end
defp insert_participants_stats_into_events([], nb_events) do
IO.puts("\nFinished inserting participant stats for #{nb_events} events!\n")
end
defp count_participants(%Event{id: event_id}, role) when is_atom(role) do
event_id
|> Events.count_participants_query()
|> Events.filter_role(role)
|> Repo.aggregate(:count, :id)
end
end

View File

@@ -0,0 +1,49 @@
defmodule Mix.Tasks.Mobilizon.SetupSearch do
@moduledoc """
Temporary task to insert search data from existing events
This task will be removed in version 1.0.0-beta.3
"""
use Mix.Task
alias Mobilizon.Service.Search
alias Mobilizon.Storage.Repo
alias Mobilizon.Events.Event
import Ecto.Query
require Logger
@shortdoc "Insert search data"
def run([]) do
Mix.Task.run("app.start")
events =
Event
|> preload([e], :tags)
|> Repo.all()
nb_events = length(events)
IO.puts("\nStarting setting up search for #{nb_events} events, this can take a while…\n")
insert_search_event(events, nb_events)
end
defp insert_search_event([%Event{url: url} = event | events], nb_events) do
case Search.insert_search_event(event) do
{:ok, _} ->
Logger.debug("Added event #{url} to the search")
{:error, res} ->
Logger.error("Error while adding event #{url} to the search: #{inspect(res)}")
end
ProgressBar.render(nb_events - length(events), nb_events)
insert_search_event(events, nb_events)
end
defp insert_search_event([], nb_events) do
IO.puts("\nFinished setting up search for #{nb_events} events!\n")
end
end

View File

@@ -5,18 +5,20 @@ defmodule Mix.Tasks.Mobilizon.Toot do
use Mix.Task
alias MobilizonWeb.API
alias MobilizonWeb.API.Comments
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
require Logger
@shortdoc "Toot to an user"
def run([from, content]) do
def run([from, text]) do
Mix.Task.run("app.start")
case API.Comments.create_comment(from, content) do
{:ok, _, _} ->
Mix.shell().info("Tooted")
with {:local_actor, %Actor{} = actor} <- {:local_actor, Actors.get_local_actor_by_name(from)},
{:ok, _, _} <- Comments.create_comment(%{actor: actor, text: text}) do
Mix.shell().info("Tooted")
else
{:local_actor, _, _} ->
Mix.shell().error("Failed to toot.\nActor #{from} doesn't exist")

View File

@@ -13,6 +13,7 @@ defmodule Mobilizon.Actors.Actor do
alias Mobilizon.Media.File
alias Mobilizon.Reports.{Note, Report}
alias Mobilizon.Users.User
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes
@@ -46,6 +47,7 @@ defmodule Mobilizon.Actors.Actor do
created_reports: [Report.t()],
subject_reports: [Report.t()],
report_notes: [Note.t()],
mentions: [Mention.t()],
memberships: [t]
}
@@ -139,6 +141,7 @@ defmodule Mobilizon.Actors.Actor do
has_many(:created_reports, Report, foreign_key: :reporter_id)
has_many(:subject_reports, Report, foreign_key: :reported_id)
has_many(:report_notes, Note, foreign_key: :moderator_id)
has_many(:mentions, Mention)
many_to_many(:memberships, __MODULE__, join_through: Member)
timestamps()

View File

@@ -88,7 +88,10 @@ defmodule Mobilizon.Actors do
"""
@spec get_actor_by_url(String.t(), boolean) ::
{:ok, Actor.t()} | {:error, :actor_not_found}
def get_actor_by_url(url, preload \\ false) do
def get_actor_by_url(url, preload \\ false)
def get_actor_by_url(nil, _preload), do: {:error, :actor_not_found}
def get_actor_by_url(url, preload) do
case Repo.get_by(Actor, url: url) do
nil ->
{:error, :actor_not_found}

View File

@@ -65,8 +65,7 @@ defmodule Mobilizon.Addresses.Address do
@spec set_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp set_url(%Ecto.Changeset{changes: changes} = changeset) do
uuid = Ecto.UUID.generate()
url = Map.get(changes, :url, "#{MobilizonWeb.Endpoint.url()}/address/#{uuid}")
url = Map.get(changes, :url, "#{MobilizonWeb.Endpoint.url()}/address/#{Ecto.UUID.generate()}")
put_change(changeset, :url, url)
end

View File

@@ -8,7 +8,8 @@ defmodule Mobilizon.Events.Comment do
import Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.{Comment, CommentVisibility, Event}
alias Mobilizon.Events.{Comment, CommentVisibility, Event, Tag}
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes
@@ -22,6 +23,8 @@ defmodule Mobilizon.Events.Comment do
actor: Actor.t(),
attributed_to: Actor.t(),
event: Event.t(),
tags: [Tag.t()],
mentions: [Mention.t()],
in_reply_to_comment: t,
origin_comment: t
}
@@ -42,6 +45,8 @@ defmodule Mobilizon.Events.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)
many_to_many(:tags, Tag, join_through: "comments_tags", on_replace: :delete)
has_many(:mentions, Mention)
timestamps(type: :utc_datetime)
end
@@ -57,16 +62,45 @@ defmodule Mobilizon.Events.Comment do
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = comment, attrs) do
uuid = attrs["uuid"] || Ecto.UUID.generate()
url = attrs["url"] || generate_url(uuid)
uuid = Map.get(attrs, :uuid) || Ecto.UUID.generate()
url = Map.get(attrs, :url) || generate_url(uuid)
comment
|> cast(attrs, @attrs)
|> put_change(:uuid, uuid)
|> put_change(:url, url)
|> put_tags(attrs)
|> put_mentions(attrs)
|> validate_required(@required_attrs)
end
@spec generate_url(String.t()) :: String.t()
defp generate_url(uuid), do: Routes.page_url(Endpoint, :comment, uuid)
@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
@spec put_mentions(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
defp put_mentions(changeset, %{"mentions" => mentions}),
do: put_assoc(changeset, :mentions, Enum.map(mentions, &process_mention/1))
defp put_mentions(changeset, %{mentions: mentions}),
do: put_assoc(changeset, :mentions, Enum.map(mentions, &process_mention/1))
defp put_mentions(changeset, _), do: changeset
# We need a changeset instead of a raw struct because of slug which is generated in changeset
defp process_tag(tag) do
Tag.changeset(%Tag{}, tag)
end
defp process_mention(tag) do
Mention.changeset(%Mention{}, tag)
end
end

View File

@@ -6,22 +6,31 @@ defmodule Mobilizon.Events.Event do
use Ecto.Schema
import Ecto.Changeset
alias Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
alias Mobilizon.Addresses
alias Mobilizon.Events.{
EventOptions,
EventStatus,
EventVisibility,
JoinOptions,
EventParticipantStats,
Participant,
Session,
Tag,
Track
}
alias Mobilizon.Media
alias Mobilizon.Media.Picture
alias Mobilizon.Mention
alias MobilizonWeb.Endpoint
alias MobilizonWeb.Router.Helpers, as: Routes
@type t :: %__MODULE__{
url: String.t(),
@@ -47,30 +56,15 @@ defmodule Mobilizon.Events.Event do
picture: Picture.t(),
tracks: [Track.t()],
sessions: [Session.t()],
mentions: [Mention.t()],
tags: [Tag.t()],
participants: [Actor.t()]
}
@required_attrs [:title, :begins_on, :organizer_actor_id, :url, :uuid]
@update_required_attrs [:title, :begins_on, :organizer_actor_id]
@required_attrs @update_required_attrs ++ [:url, :uuid]
@optional_attrs [
:slug,
:description,
:ends_on,
:category,
:status,
:draft,
:visibility,
:publish_at,
:online_address,
:phone_address,
:picture_id,
:physical_address_id
]
@attrs @required_attrs ++ @optional_attrs
@update_required_attrs @required_attrs
@update_optional_attrs [
:slug,
:description,
:ends_on,
@@ -85,7 +79,9 @@ defmodule Mobilizon.Events.Event do
:picture_id,
:physical_address_id
]
@update_attrs @update_required_attrs ++ @update_optional_attrs
@attrs @required_attrs ++ @optional_attrs
@update_attrs @update_required_attrs ++ @optional_attrs
schema "events" do
field(:url, :string)
@@ -105,13 +101,15 @@ defmodule Mobilizon.Events.Event do
field(:phone_address, :string)
field(:category, :string)
embeds_one(:options, EventOptions, on_replace: :update)
embeds_one(:options, EventOptions, on_replace: :delete)
embeds_one(:participant_stats, EventParticipantStats, on_replace: :update)
belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
belongs_to(:physical_address, Address)
belongs_to(:picture, Picture)
belongs_to(:physical_address, Address, on_replace: :update)
belongs_to(:picture, Picture, on_replace: :update)
has_many(:tracks, Track)
has_many(:sessions, Session)
has_many(:mentions, Mention)
many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
many_to_many(:participants, Actor, join_through: Participant)
@@ -119,28 +117,41 @@ defmodule Mobilizon.Events.Event do
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
@spec changeset(t, map) :: Changeset.t()
def changeset(%__MODULE__{} = event, attrs) do
attrs = Map.update(attrs, :uuid, Ecto.UUID.generate(), & &1)
attrs = Map.update(attrs, :url, Routes.page_url(Endpoint, :event, attrs.uuid), & &1)
event
|> cast(attrs, @attrs)
|> cast_embed(:options)
|> common_changeset(attrs)
|> put_creator_if_published(:create)
|> validate_required(@required_attrs)
|> validate_lengths()
end
@doc false
@spec update_changeset(t, map) :: Ecto.Changeset.t()
@spec update_changeset(t, map) :: Changeset.t()
def update_changeset(%__MODULE__{} = event, attrs) do
event
|> Ecto.Changeset.cast(attrs, @update_attrs)
|> cast_embed(:options)
|> put_tags(attrs)
|> cast(attrs, @update_attrs)
|> common_changeset(attrs)
|> put_creator_if_published(:update)
|> validate_required(@update_required_attrs)
|> validate_lengths()
end
@spec validate_lengths(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp validate_lengths(%Ecto.Changeset{} = changeset) do
@spec common_changeset(Changeset.t(), map) :: Changeset.t()
defp common_changeset(%Changeset{} = changeset, attrs) do
changeset
|> cast_embed(:options)
|> put_tags(attrs)
|> put_address(attrs)
|> put_picture(attrs)
end
@spec validate_lengths(Changeset.t()) :: Changeset.t()
defp validate_lengths(%Changeset{} = changeset) do
changeset
|> validate_length(:title, min: 3, max: 200)
|> validate_length(:online_address, min: 3, max: 2000)
@@ -161,7 +172,80 @@ defmodule Mobilizon.Events.Event do
def can_be_managed_by(_event, _actor), do: {:event_can_be_managed, false}
@spec put_tags(Ecto.Changeset.t(), map) :: Ecto.Changeset.t()
defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags)
defp put_tags(changeset, _), do: changeset
@spec put_tags(Changeset.t(), map) :: Changeset.t()
defp put_tags(%Changeset{} = changeset, %{tags: tags}),
do: put_assoc(changeset, :tags, Enum.map(tags, &process_tag/1))
defp put_tags(%Changeset{} = changeset, _), do: changeset
# We need a changeset instead of a raw struct because of slug which is generated in changeset
defp process_tag(tag) do
Tag.changeset(%Tag{}, tag)
end
# In case the provided addresses is an existing one
@spec put_address(Changeset.t(), map) :: Changeset.t()
defp put_address(%Changeset{} = changeset, %{physical_address: %{id: id} = _physical_address}) do
case Addresses.get_address!(id) do
%Address{} = address ->
put_assoc(changeset, :physical_address, address)
_ ->
changeset
end
end
# In case it's a new address
defp put_address(%Changeset{} = changeset, _attrs) do
cast_assoc(changeset, :physical_address)
end
# In case the provided picture is an existing one
@spec put_picture(Changeset.t(), map) :: Changeset.t()
defp put_picture(%Changeset{} = changeset, %{picture: %{picture_id: id} = _picture}) do
case Media.get_picture!(id) do
%Picture{} = picture ->
put_assoc(changeset, :picture, picture)
_ ->
changeset
end
end
# In case it's a new picture
defp put_picture(%Changeset{} = changeset, _attrs) do
cast_assoc(changeset, :picture)
end
# Created or updated with draft parameter: don't publish
defp put_creator_if_published(
%Changeset{changes: %{draft: true}} = changeset,
_action
) do
cast_embed(changeset, :participant_stats)
end
# Created with any other value: publish
defp put_creator_if_published(
%Changeset{} = changeset,
:create
) do
changeset
|> put_embed(:participant_stats, %{creator: 1})
end
# Updated from draft false to true: publish
defp put_creator_if_published(
%Changeset{
data: %{draft: false},
changes: %{draft: true}
} = changeset,
:update
) do
changeset
|> put_embed(:participant_stats, %{creator: 1})
end
defp put_creator_if_published(%Changeset{} = changeset, _),
do: cast_embed(changeset, :participant_stats)
end

View File

@@ -0,0 +1,44 @@
defmodule Mobilizon.Events.EventParticipantStats do
@moduledoc """
Participation stats on event
"""
use Ecto.Schema
import Ecto.Changeset
@type t :: %__MODULE__{
not_approved: integer(),
rejected: integer(),
participant: integer(),
moderator: integer(),
administrator: integer(),
creator: integer()
}
@attrs [
:not_approved,
:rejected,
:participant,
:moderator,
:administrator,
:moderator,
:creator
]
@primary_key false
@derive Jason.Encoder
embedded_schema do
field(:not_approved, :integer, default: 0)
field(:rejected, :integer, default: 0)
field(:participant, :integer, default: 0)
field(:moderator, :integer, default: 0)
field(:administrator, :integer, default: 0)
field(:creator, :integer, default: 0)
end
@doc false
@spec changeset(t, map) :: Ecto.Changeset.t()
def changeset(%__MODULE__{} = event_options, attrs) do
cast(event_options, attrs, @attrs)
end
end

View File

@@ -7,6 +7,7 @@ defmodule Mobilizon.Events do
import Ecto.Query
import EctoEnum
alias Ecto.{Multi, Changeset}
import Mobilizon.Storage.Ecto
@@ -17,6 +18,7 @@ defmodule Mobilizon.Events do
alias Mobilizon.Events.{
Comment,
Event,
EventParticipantStats,
FeedToken,
Participant,
Session,
@@ -82,6 +84,8 @@ defmodule Mobilizon.Events do
@event_preloads [
:organizer_actor,
:attributed_to,
:mentions,
:sessions,
:tracks,
:tags,
@@ -90,7 +94,7 @@ defmodule Mobilizon.Events do
:picture
]
@comment_preloads [:actor, :attributed_to, :in_reply_to_comment]
@comment_preloads [:actor, :attributed_to, :in_reply_to_comment, :tags, :mentions]
@doc """
Gets a single event.
@@ -235,81 +239,103 @@ defmodule Mobilizon.Events do
|> Repo.one()
end
def get_or_create_event(%{"url" => url} = attrs) do
case Repo.get_by(Event, url: url) do
%Event{} = event -> {:ok, Repo.preload(event, @event_preloads)}
nil -> create_event(attrs)
end
end
@doc """
Creates an event.
"""
@spec create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
@spec create_event(map) :: {:ok, Event.t()} | {:error, Changeset.t()}
def create_event(attrs \\ %{}) do
with {:ok, %Event{draft: false} = event} <- do_create_event(attrs),
{:ok, %Participant{} = _participant} <-
create_participant(%{
actor_id: event.organizer_actor_id,
role: :creator,
event_id: event.id
}) do
with {:ok, %{insert: %Event{} = event}} <- do_create_event(attrs),
%Event{} = event <- Repo.preload(event, @event_preloads) do
Task.start(fn -> Search.insert_search_event(event) end)
{:ok, event}
else
# We don't create a creator participant if the event is a draft
{:ok, %Event{draft: true} = event} -> {:ok, event}
err -> err
end
end
@spec do_create_event(map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
# We start by inserting the event and then insert a first participant if the event is not a draft
@spec do_create_event(map) :: {:ok, Event.t()} | {:error, Changeset.t()}
defp do_create_event(attrs) do
with {:ok, %Event{} = event} <-
%Event{}
|> Event.changeset(attrs)
|> Ecto.Changeset.put_assoc(:tags, Map.get(attrs, "tags", []))
|> Repo.insert(),
%Event{} = event <-
Repo.preload(event, [:tags, :organizer_actor, :physical_address, :picture]) do
{:ok, event}
end
Multi.new()
|> Multi.insert(:insert, Event.changeset(%Event{}, attrs))
|> Multi.run(:write, fn _repo, %{insert: %Event{draft: draft} = event} ->
with {:is_draft, false} <- {:is_draft, draft},
{:ok, %Participant{} = participant} <-
create_participant(
%{
event_id: event.id,
role: :creator,
actor_id: event.organizer_actor_id
},
false
) do
{:ok, participant}
else
{:is_draft, true} -> {:ok, nil}
err -> err
end
end)
|> Repo.transaction()
end
@doc """
Updates an event.
We start by updating the event and then insert a first participant if the event is not a draft anymore
"""
@spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
def update_event(
%Event{draft: old_draft_status, id: event_id, organizer_actor_id: organizer_actor_id} =
old_event,
attrs
) do
with %Ecto.Changeset{changes: changes} = changeset <-
old_event |> Repo.preload(:tags) |> Event.update_changeset(attrs) do
with {:ok, %Event{draft: new_draft_status} = new_event} <- Repo.update(changeset) do
# If the event is no longer a draft
if old_draft_status == true && new_draft_status == false do
{:ok, %Participant{} = _participant} =
create_participant(%{
event_id: event_id,
role: :creator,
actor_id: organizer_actor_id
})
end
@spec update_event(Event.t(), map) :: {:ok, Event.t()} | {:error, Changeset.t()}
def update_event(%Event{} = old_event, attrs) do
with %Changeset{changes: changes} = changeset <-
Event.update_changeset(Repo.preload(old_event, :tags), attrs),
{:ok, %{update: %Event{} = new_event}} <-
Multi.new()
|> Multi.update(
:update,
changeset
)
|> Multi.run(:write, fn _repo, %{update: %Event{draft: draft} = event} ->
with {:is_draft, false} <- {:is_draft, draft},
{:ok, %Participant{} = participant} <-
create_participant(
%{
event_id: event.id,
role: :creator,
actor_id: event.organizer_actor_id
},
false
) do
{:ok, participant}
else
{:is_draft, true} -> {:ok, nil}
err -> err
end
end)
|> Repo.transaction() do
Cachex.del(:ics, "event_#{new_event.uuid}")
Cachex.del(:ics, "event_#{new_event.uuid}")
Mobilizon.Service.Events.Tool.calculate_event_diff_and_send_notifications(
old_event,
new_event,
changes
)
Mobilizon.Service.Events.Tool.calculate_event_diff_and_send_notifications(
old_event,
new_event,
changes
)
Task.start(fn -> Search.update_search_event(new_event) end)
Task.start(fn -> Search.update_search_event(new_event) end)
{:ok, new_event}
end
{:ok, Repo.preload(new_event, @event_preloads)}
end
end
@doc """
Deletes an event.
"""
@spec delete_event(Event.t()) :: {:ok, Event.t()} | {:error, Ecto.Changeset.t()}
@spec delete_event(Event.t()) :: {:ok, Event.t()} | {:error, Changeset.t()}
def delete_event(%Event{} = event), do: Repo.delete(event)
@doc """
@@ -450,7 +476,7 @@ defmodule Mobilizon.Events do
@doc """
Gets an existing tag or creates the new one.
"""
@spec get_or_create_tag(map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
@spec get_or_create_tag(map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def get_or_create_tag(%{"name" => "#" <> title}) do
case Repo.get_by(Tag, title: title) do
%Tag{} = tag ->
@@ -461,10 +487,24 @@ defmodule Mobilizon.Events do
end
end
@doc """
Gets an existing tag or creates the new one.
"""
@spec get_or_create_tag(String.t()) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def get_or_create_tag(title) do
case Repo.get_by(Tag, title: title) do
%Tag{} = tag ->
{:ok, tag}
nil ->
create_tag(%{"title" => title})
end
end
@doc """
Creates a tag.
"""
@spec create_tag(map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
@spec create_tag(map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def create_tag(attrs \\ %{}) do
%Tag{}
|> Tag.changeset(attrs)
@@ -474,7 +514,7 @@ defmodule Mobilizon.Events do
@doc """
Updates a tag.
"""
@spec update_tag(Tag.t(), map) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
@spec update_tag(Tag.t(), map) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def update_tag(%Tag{} = tag, attrs) do
tag
|> Tag.changeset(attrs)
@@ -484,7 +524,7 @@ defmodule Mobilizon.Events do
@doc """
Deletes a tag.
"""
@spec delete_tag(Tag.t()) :: {:ok, Tag.t()} | {:error, Ecto.Changeset.t()}
@spec delete_tag(Tag.t()) :: {:ok, Tag.t()} | {:error, Changeset.t()}
def delete_tag(%Tag{} = tag), do: Repo.delete(tag)
@doc """
@@ -524,7 +564,7 @@ defmodule Mobilizon.Events do
@doc """
Creates a relation between two tags.
"""
@spec create_tag_relation(map) :: {:ok, TagRelation.t()} | {:error, Ecto.Changeset.t()}
@spec create_tag_relation(map) :: {:ok, TagRelation.t()} | {:error, Changeset.t()}
def create_tag_relation(attrs \\ {}) do
%TagRelation{}
|> TagRelation.changeset(attrs)
@@ -538,7 +578,7 @@ defmodule Mobilizon.Events do
Removes a tag relation.
"""
@spec delete_tag_relation(TagRelation.t()) ::
{:ok, TagRelation.t()} | {:error, Ecto.Changeset.t()}
{:ok, TagRelation.t()} | {:error, Changeset.t()}
def delete_tag_relation(%TagRelation{} = tag_relation) do
Repo.delete(tag_relation)
end
@@ -763,7 +803,7 @@ defmodule Mobilizon.Events do
end
@doc """
Counts participant participants.
Counts participant participants (participants with no extra role)
"""
@spec count_participant_participants(integer | String.t()) :: integer
def count_participant_participants(event_id) do
@@ -773,17 +813,6 @@ defmodule Mobilizon.Events do
|> Repo.aggregate(:count, :id)
end
@doc """
Counts unapproved participants.
"""
@spec count_unapproved_participants(integer | String.t()) :: integer
def count_unapproved_participants(event_id) do
event_id
|> count_participants_query()
|> filter_unapproved_role()
|> Repo.aggregate(:count, :id)
end
@doc """
Counts rejected participants.
"""
@@ -805,12 +834,40 @@ defmodule Mobilizon.Events do
@doc """
Creates a participant.
"""
@spec create_participant(map) :: {:ok, Participant.t()} | {:error, Ecto.Changeset.t()}
def create_participant(attrs \\ %{}) do
with {:ok, %Participant{} = participant} <-
%Participant{}
|> Participant.changeset(attrs)
|> Repo.insert() do
@spec create_participant(map) :: {:ok, Participant.t()} | {:error, Changeset.t()}
def create_participant(attrs \\ %{}, update_event_participation_stats \\ true) do
with {:ok, %{participant: %Participant{} = participant}} <-
Multi.new()
|> Multi.insert(:participant, Participant.changeset(%Participant{}, attrs))
|> Multi.run(:update_event_participation_stats, fn _repo,
%{
participant:
%Participant{
role: role,
event_id: event_id
} = _participant
} ->
with {:update_event_participation_stats, true} <-
{:update_event_participation_stats, update_event_participation_stats},
{:ok, %Event{} = event} <- get_event(event_id),
%EventParticipantStats{} = participant_stats <-
Map.get(event, :participant_stats),
%EventParticipantStats{} = participant_stats <-
Map.update(participant_stats, role, 0, &(&1 + 1)),
{:ok, %Event{} = event} <-
event
|> Event.update_changeset(%{
participant_stats: Map.from_struct(participant_stats)
})
|> Repo.update() do
{:ok, event}
else
{:update_event_participation_stats, false} -> {:ok, nil}
{:error, :event_not_found} -> {:error, :event_not_found}
err -> {:error, err}
end
end)
|> Repo.transaction() do
{:ok, Repo.preload(participant, [:event, :actor])}
end
end
@@ -819,7 +876,7 @@ defmodule Mobilizon.Events do
Updates a participant.
"""
@spec update_participant(Participant.t(), map) ::
{:ok, Participant.t()} | {:error, Ecto.Changeset.t()}
{:ok, Participant.t()} | {:error, Changeset.t()}
def update_participant(%Participant{} = participant, attrs) do
participant
|> Participant.changeset(attrs)
@@ -830,7 +887,7 @@ defmodule Mobilizon.Events do
Deletes a participant.
"""
@spec delete_participant(Participant.t()) ::
{:ok, Participant.t()} | {:error, Ecto.Changeset.t()}
{:ok, Participant.t()} | {:error, Changeset.t()}
def delete_participant(%Participant{} = participant), do: Repo.delete(participant)
@doc """
@@ -843,7 +900,7 @@ defmodule Mobilizon.Events do
@doc """
Creates a session.
"""
@spec create_session(map) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()}
@spec create_session(map) :: {:ok, Session.t()} | {:error, Changeset.t()}
def create_session(attrs \\ %{}) do
%Session{}
|> Session.changeset(attrs)
@@ -853,7 +910,7 @@ defmodule Mobilizon.Events do
@doc """
Updates a session.
"""
@spec update_session(Session.t(), map) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()}
@spec update_session(Session.t(), map) :: {:ok, Session.t()} | {:error, Changeset.t()}
def update_session(%Session{} = session, attrs) do
session
|> Session.changeset(attrs)
@@ -863,7 +920,7 @@ defmodule Mobilizon.Events do
@doc """
Deletes a session.
"""
@spec delete_session(Session.t()) :: {:ok, Session.t()} | {:error, Ecto.Changeset.t()}
@spec delete_session(Session.t()) :: {:ok, Session.t()} | {:error, Changeset.t()}
def delete_session(%Session{} = session), do: Repo.delete(session)
@doc """
@@ -892,7 +949,7 @@ defmodule Mobilizon.Events do
@doc """
Creates a track.
"""
@spec create_track(map) :: {:ok, Track.t()} | {:error, Ecto.Changeset.t()}
@spec create_track(map) :: {:ok, Track.t()} | {:error, Changeset.t()}
def create_track(attrs \\ %{}) do
%Track{}
|> Track.changeset(attrs)
@@ -902,7 +959,7 @@ defmodule Mobilizon.Events do
@doc """
Updates a track.
"""
@spec update_track(Track.t(), map) :: {:ok, Track.t()} | {:error, Ecto.Changeset.t()}
@spec update_track(Track.t(), map) :: {:ok, Track.t()} | {:error, Changeset.t()}
def update_track(%Track{} = track, attrs) do
track
|> Track.changeset(attrs)
@@ -912,7 +969,7 @@ defmodule Mobilizon.Events do
@doc """
Deletes a track.
"""
@spec delete_track(Track.t()) :: {:ok, Track.t()} | {:error, Ecto.Changeset.t()}
@spec delete_track(Track.t()) :: {:ok, Track.t()} | {:error, Changeset.t()}
def delete_track(%Track{} = track), do: Repo.delete(track)
@doc """
@@ -931,6 +988,13 @@ defmodule Mobilizon.Events do
|> Repo.all()
end
@doc """
Gets a single comment.
"""
@spec get_comment(integer | String.t()) :: Comment.t()
def get_comment(nil), do: nil
def get_comment(id), do: Repo.get(Comment, id)
@doc """
Gets a single comment.
Raises `Ecto.NoResultsError` if the comment does not exist.
@@ -994,10 +1058,17 @@ defmodule Mobilizon.Events do
|> Repo.preload(@comment_preloads)
end
def get_or_create_comment(%{"url" => url} = attrs) do
case Repo.get_by(Comment, url: url) do
%Comment{} = comment -> {:ok, Repo.preload(comment, @comment_preloads)}
nil -> create_comment(attrs)
end
end
@doc """
Creates a comment.
"""
@spec create_comment(map) :: {:ok, Comment.t()} | {:error, Ecto.Changeset.t()}
@spec create_comment(map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
def create_comment(attrs \\ %{}) do
with {:ok, %Comment{} = comment} <-
%Comment{}
@@ -1011,7 +1082,7 @@ defmodule Mobilizon.Events do
@doc """
Updates a comment.
"""
@spec update_comment(Comment.t(), map) :: {:ok, Comment.t()} | {:error, Ecto.Changeset.t()}
@spec update_comment(Comment.t(), map) :: {:ok, Comment.t()} | {:error, Changeset.t()}
def update_comment(%Comment{} = comment, attrs) do
comment
|> Comment.changeset(attrs)
@@ -1021,7 +1092,7 @@ defmodule Mobilizon.Events do
@doc """
Deletes a comment.
"""
@spec delete_comment(Comment.t()) :: {:ok, Comment.t()} | {:error, Ecto.Changeset.t()}
@spec delete_comment(Comment.t()) :: {:ok, Comment.t()} | {:error, Changeset.t()}
def delete_comment(%Comment{} = comment), do: Repo.delete(comment)
@doc """
@@ -1097,7 +1168,7 @@ defmodule Mobilizon.Events do
@doc """
Creates a feed token.
"""
@spec create_feed_token(map) :: {:ok, FeedToken.t()} | {:error, Ecto.Changeset.t()}
@spec create_feed_token(map) :: {:ok, FeedToken.t()} | {:error, Changeset.t()}
def create_feed_token(attrs \\ %{}) do
attrs = Map.put(attrs, "token", Ecto.UUID.generate())
@@ -1110,7 +1181,7 @@ defmodule Mobilizon.Events do
Updates a feed token.
"""
@spec update_feed_token(FeedToken.t(), map) ::
{:ok, FeedToken.t()} | {:error, Ecto.Changeset.t()}
{:ok, FeedToken.t()} | {:error, Changeset.t()}
def update_feed_token(%FeedToken{} = feed_token, attrs) do
feed_token
|> FeedToken.changeset(attrs)
@@ -1120,7 +1191,7 @@ defmodule Mobilizon.Events do
@doc """
Deletes a feed token.
"""
@spec delete_feed_token(FeedToken.t()) :: {:ok, FeedToken.t()} | {:error, Ecto.Changeset.t()}
@spec delete_feed_token(FeedToken.t()) :: {:ok, FeedToken.t()} | {:error, Changeset.t()}
def delete_feed_token(%FeedToken{} = feed_token), do: Repo.delete(feed_token)
@doc """
@@ -1354,7 +1425,7 @@ defmodule Mobilizon.Events do
end
@spec count_participants_query(integer) :: Ecto.Query.t()
defp count_participants_query(event_id) do
def count_participants_query(event_id) do
from(p in Participant, where: p.event_id == ^event_id)
end
@@ -1385,17 +1456,10 @@ defmodule Mobilizon.Events do
end
defp public_comments_for_actor_query(actor_id) do
from(
c in Comment,
where: c.actor_id == ^actor_id and c.visibility in ^@public_visibility,
order_by: [desc: :id],
preload: [
:actor,
:in_reply_to_comment,
:origin_comment,
:event
]
)
Comment
|> where([c], c.actor_id == ^actor_id and c.visibility in ^@public_visibility)
|> order_by([c], desc: :id)
|> preload_for_comment()
end
@spec list_participants_for_event_query(String.t()) :: Ecto.Query.t()
@@ -1498,31 +1562,30 @@ defmodule Mobilizon.Events do
@spec filter_approved_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_approved_role(query) do
from(p in query, where: p.role not in ^[:not_approved, :rejected])
filter_role(query, [:not_approved, :rejected])
end
@spec filter_participant_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_participant_role(query) do
from(p in query, where: p.role == ^:participant)
end
@spec filter_unapproved_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_unapproved_role(query) do
from(p in query, where: p.role == ^:not_approved)
filter_role(query, :participant)
end
@spec filter_rejected_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_rejected_role(query) do
from(p in query, where: p.role == ^:rejected)
filter_role(query, :rejected)
end
@spec filter_role(Ecto.Query.t(), list(atom())) :: Ecto.Query.t()
defp filter_role(query, []), do: query
def filter_role(query, []), do: query
defp filter_role(query, roles) do
def filter_role(query, roles) when is_list(roles) do
where(query, [p], p.role in ^roles)
end
def filter_role(query, role) when is_atom(role) do
from(p in query, where: p.role == ^role)
end
defp participation_filter_begins_on(query, nil, nil),
do: participation_order_begins_on_desc(query)

View File

@@ -0,0 +1,53 @@
defmodule Mobilizon.Mention do
@moduledoc """
The Mentions context.
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Events.Comment
alias Mobilizon.Storage.Repo
@type t :: %__MODULE__{
silent: boolean,
actor: Actor.t(),
event: Event.t(),
comment: Comment.t()
}
@required_attrs [:actor_id]
@optional_attrs [:silent, :event_id, :comment_id]
@attrs @required_attrs ++ @optional_attrs
schema "mentions" do
field(:silent, :boolean, default: false)
belongs_to(:actor, Actor)
belongs_to(:event, Event)
belongs_to(:comment, Comment)
timestamps()
end
@doc false
def changeset(event, attrs) do
event
|> cast(attrs, @attrs)
# TODO: Enforce having either event_id or comment_id
|> validate_required(@required_attrs)
end
@doc """
Creates a new mention
"""
@spec create_mention(map()) :: {:ok, t} | {:error, Ecto.Changeset.t()}
def create_mention(args) do
with {:ok, %__MODULE__{} = mention} <-
%__MODULE__{}
|> changeset(args)
|> Repo.insert() do
{:ok, Repo.preload(mention, [:actor, :event, :comment])}
end
end
end

View File

@@ -2,55 +2,18 @@ defmodule MobilizonWeb.API.Comments do
@moduledoc """
API for Comments.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias MobilizonWeb.API.Utils
alias Mobilizon.Service.ActivityPub.Activity
@doc """
Create a comment
Creates a comment from an actor and a status
"""
@spec create_comment(String.t(), String.t(), String.t()) ::
@spec create_comment(map()) ::
{:ok, Activity.t(), Comment.t()} | any()
def create_comment(
from_username,
status,
visibility \\ :public,
in_reply_to_comment_URL \\ nil
) do
with {:local_actor, %Actor{url: url} = actor} <-
{:local_actor, Actors.get_local_actor_by_name(from_username)},
in_reply_to_comment <- get_in_reply_to_comment(in_reply_to_comment_URL),
{content_html, tags, to, cc} <-
Utils.prepare_content(actor, status, visibility, [], in_reply_to_comment),
comment <-
ActivityPubUtils.make_comment_data(
url,
to,
content_html,
in_reply_to_comment,
tags,
cc
) do
ActivityPub.create(%{
to: to,
actor: actor,
object: comment,
local: true
})
end
end
@spec get_in_reply_to_comment(nil) :: nil
defp get_in_reply_to_comment(nil), do: nil
@spec get_in_reply_to_comment(String.t()) :: Comment.t()
defp get_in_reply_to_comment(in_reply_to_comment_url) do
ActivityPub.fetch_object_from_url(in_reply_to_comment_url)
def create_comment(args) do
ActivityPub.create(:comment, args, true)
end
end

View File

@@ -3,37 +3,24 @@ defmodule MobilizonWeb.API.Events do
API for Events.
"""
alias Mobilizon.Events.Event
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias MobilizonWeb.API.Utils
alias Mobilizon.Service.ActivityPub.Utils
@doc """
Create an event
"""
@spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any()
def create_event(%{organizer_actor: organizer_actor} = args) do
with args <- prepare_args(args),
event <-
ActivityPubUtils.make_event_data(
args.organizer_actor.url,
%{to: args.to, cc: args.cc},
args.title,
args.content_html,
args.picture,
args.tags,
args.metadata
) do
ActivityPub.create(%{
to: args.to,
actor: organizer_actor,
object: event,
# For now we don't federate drafts but it will be needed if we want to edit them as groups
local: args.metadata.draft == false
})
def create_event(args) do
with organizer_actor <- Map.get(args, :organizer_actor),
args <-
Map.update(args, :picture, nil, fn picture ->
process_picture(picture, organizer_actor)
end) do
# For now we don't federate drafts but it will be needed if we want to edit them as groups
ActivityPub.create(:event, args, args.draft == false)
end
end
@@ -41,65 +28,13 @@ defmodule MobilizonWeb.API.Events do
Update an event
"""
@spec update_event(map(), Event.t()) :: {:ok, Activity.t(), Event.t()} | any()
def update_event(
%{
organizer_actor: organizer_actor
} = args,
%Event{} = event
) do
with args <- Map.put(args, :tags, Map.get(args, :tags, [])),
args <- prepare_args(Map.merge(event, args)),
event <-
ActivityPubUtils.make_event_data(
args.organizer_actor.url,
%{to: args.to, cc: args.cc},
args.title,
args.content_html,
args.picture,
args.tags,
args.metadata,
event.uuid,
event.url
) do
ActivityPub.update(%{
to: args.to,
actor: organizer_actor.url,
cc: [],
object: event,
local: args.metadata.draft == false
})
end
end
defp prepare_args(args) do
with %Actor{} = organizer_actor <- Map.get(args, :organizer_actor),
title <- args |> Map.get(:title, "") |> HtmlSanitizeEx.strip_tags() |> String.trim(),
visibility <- Map.get(args, :visibility, :public),
description <- Map.get(args, :description),
tags <- Map.get(args, :tags),
{content_html, tags, to, cc} <-
Utils.prepare_content(organizer_actor, description, visibility, tags, nil) do
%{
title: title,
content_html: content_html,
picture: Map.get(args, :picture),
tags: tags,
organizer_actor: organizer_actor,
to: to,
cc: cc,
metadata: %{
begins_on: Map.get(args, :begins_on),
ends_on: Map.get(args, :ends_on),
physical_address: Map.get(args, :physical_address),
category: Map.get(args, :category),
options: Map.get(args, :options),
join_options: Map.get(args, :join_options),
status: Map.get(args, :status),
online_address: Map.get(args, :online_address),
phone_address: Map.get(args, :phone_address),
draft: Map.get(args, :draft)
}
}
def update_event(args, %Event{} = event) do
with organizer_actor <- Map.get(args, :organizer_actor),
args <-
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)
end
end
@@ -111,4 +46,15 @@ defmodule MobilizonWeb.API.Events do
def delete_event(%Event{} = event, federate \\ true) do
ActivityPub.delete(event, federate)
end
defp process_picture(nil, _), do: nil
defp process_picture(%{picture_id: _picture_id} = args, _), do: args
defp process_picture(%{picture: picture}, %Actor{id: actor_id}) do
%{
file:
picture |> Map.get(:file) |> Utils.make_picture_data(description: Map.get(picture, :name)),
actor_id: actor_id
}
end
end

View File

@@ -6,6 +6,7 @@ defmodule MobilizonWeb.API.Follows do
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Activity
require Logger
@@ -32,17 +33,14 @@ defmodule MobilizonWeb.API.Follows do
end
def accept(%Actor{} = follower, %Actor{} = followed) do
with %Follower{approved: false, id: follow_id, url: follow_url} = follow <-
with %Follower{approved: false} = follow <-
Actors.is_following(follower, followed),
activity_follow_url <- "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}",
data <-
ActivityPub.Utils.make_follow_data(followed, follower, follow_url),
{:ok, activity, _} <-
{:ok, %Activity{} = activity, %Follower{approved: true}} <-
ActivityPub.accept(
%{to: [follower.url], actor: followed.url, object: data},
activity_follow_url
),
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do
:follow,
follow,
%{approved: true}
) do
{:ok, activity}
else
%Follower{approved: true} ->

View File

@@ -6,38 +6,19 @@ defmodule MobilizonWeb.API.Groups do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
alias Mobilizon.Users.User
alias MobilizonWeb.API.Utils
alias Mobilizon.Service.ActivityPub.Activity
@doc """
Create a group
"""
@spec create_group(User.t(), map()) :: {:ok, Activity.t(), Group.t()} | any()
def create_group(
user,
%{
preferred_username: title,
summary: summary,
creator_actor_id: creator_actor_id,
avatar: _avatar,
banner: _banner
} = args
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, creator_actor_id),
title <- String.trim(title),
{:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
visibility <- Map.get(args, :visibility, :public),
{content_html, tags, to, cc} <-
Utils.prepare_content(actor, summary, visibility, [], nil),
group <- ActivityPubUtils.make_group_data(actor.url, to, title, content_html, tags, cc) do
ActivityPub.create(%{
to: ["https://www.w3.org/ns/activitystreams#Public"],
actor: actor,
object: group,
local: true
})
@spec create_group(map()) :: {:ok, Activity.t(), Actor.t()} | any()
def create_group(args) do
with preferred_username <-
args |> Map.get(:preferred_username) |> HtmlSanitizeEx.strip_tags() |> String.trim(),
{:existing_group, nil} <-
{:existing_group, Actors.get_local_group_by_title(preferred_username)},
{:ok, %Activity{} = activity, %Actor{} = group} <- ActivityPub.create(:group, args, true) do
{:ok, activity, group}
else
{:existing_group, _} ->
{:error, "A group with this name already exists"}

View File

@@ -38,12 +38,11 @@ defmodule MobilizonWeb.API.Participations do
) do
with {:ok, activity, _} <-
ActivityPub.accept(
%{
to: [participation.actor.url],
actor: moderator.url,
object: participation.url
},
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{participation.id}"
:join,
participation,
%{role: :participant},
true,
%{"to" => [moderator.url]}
),
{:ok, %Participant{role: :participant} = participation} <-
Events.update_participant(participation, %{"role" => :participant}),

View File

@@ -3,89 +3,9 @@ defmodule MobilizonWeb.API.Utils do
Utils for API.
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Config
alias Mobilizon.Service.Formatter
@ap_public "https://www.w3.org/ns/activitystreams#Public"
@doc """
Determines the full audience based on mentions for a public audience
Audience is:
* `to` : the mentioned actors, the eventual actor we're replying to and the public
* `cc` : the actor's followers
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :public) do
to = [@ap_public | mentions]
cc = [actor.followers_url]
if inReplyTo do
{Enum.uniq([inReplyTo.actor | to]), cc}
else
{to, cc}
end
end
@doc """
Determines the full audience based on mentions based on a unlisted audience
Audience is:
* `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
* `cc` : public
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :unlisted) do
to = [actor.followers_url | mentions]
cc = [@ap_public]
if inReplyTo do
{Enum.uniq([inReplyTo.actor | to]), cc}
else
{to, cc}
end
end
@doc """
Determines the full audience based on mentions based on a private audience
Audience is:
* `to` : the mentioned actors, actor's followers and the eventual actor we're replying to
* `cc` : none
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, inReplyTo, :private) do
{to, cc} = get_to_and_cc(actor, mentions, inReplyTo, :direct)
{[actor.followers_url | to], cc}
end
@doc """
Determines the full audience based on mentions based on a direct audience
Audience is:
* `to` : the mentioned actors and the eventual actor we're replying to
* `cc` : none
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(_actor, mentions, inReplyTo, :direct) do
if inReplyTo do
{Enum.uniq([inReplyTo.actor | mentions]), []}
else
{mentions, []}
end
end
def get_to_and_cc(_actor, mentions, _inReplyTo, {:list, _}) do
{mentions, []}
end
# def get_addressed_users(_, to) when is_list(to) do
# Actors.get(to)
# end
def get_addressed_users(mentioned_users, _), do: mentioned_users
@doc """
Creates HTML content from text and mentions
"""
@@ -126,19 +46,4 @@ defmodule MobilizonWeb.API.Utils do
{:error, "Comment must be up to #{max_size} characters"}
end
end
def prepare_content(actor, content, visibility, tags, in_reply_to) do
with content <- String.trim(content || ""),
{content_html, mentions, tags} <-
make_content_html(
content,
tags,
"text/html"
),
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.url),
addressed_users <- get_addressed_users(mentioned_users, nil),
{to, cc} <- get_to_and_cc(actor, addressed_users, in_reply_to, visibility) do
{content_html, tags, to, cc}
end
end
end

View File

@@ -4,19 +4,19 @@ defmodule MobilizonWeb.Resolvers.Comment do
"""
alias Mobilizon.Events.Comment
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Users.User
alias Mobilizon.Actors.Actor
alias MobilizonWeb.API.Comments
require Logger
def create_comment(_parent, %{text: comment, actor_username: username}, %{
context: %{current_user: %User{} = _user}
def create_comment(_parent, %{text: text, actor_id: actor_id}, %{
context: %{current_user: %User{} = user}
}) do
with {:ok, %Activity{data: %{"object" => %{"type" => "Note"} = _object}},
%Comment{} = comment} <-
Comments.create_comment(username, comment) do
with {:is_owned, %Actor{} = _organizer_actor} <- User.owns_actor(user, actor_id),
{:ok, _, %Comment{} = comment} <-
Comments.create_comment(%{actor_id: actor_id, text: text}) do
{:ok, comment}
end
end

View File

@@ -7,11 +7,8 @@ defmodule MobilizonWeb.Resolvers.Event do
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses
alias Mobilizon.Addresses.Address
alias Mobilizon.Events
alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Media.Picture
alias Mobilizon.Events.{Event, Participant, EventParticipantStats}
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Users.User
@@ -96,14 +93,8 @@ defmodule MobilizonWeb.Resolvers.Event do
{:ok, []}
end
def stats_participants_for_event(%Event{id: id}, _args, _resolution) do
{:ok,
%{
approved: Mobilizon.Events.count_approved_participants(id),
unapproved: Mobilizon.Events.count_unapproved_participants(id),
rejected: Mobilizon.Events.count_rejected_participants(id),
participants: Mobilizon.Events.count_participant_participants(id)
}}
def stats_participants_going(%EventParticipantStats{} = stats, _args, _resolution) do
{:ok, stats.participant + stats.moderator + stats.administrator + stats.creator}
end
@doc """
@@ -277,8 +268,6 @@ defmodule MobilizonWeb.Resolvers.Event do
with args <- Map.put(args, :options, args[:options] || %{}),
{:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id),
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, args_with_organizer} <- save_attached_picture(args_with_organizer),
{:ok, args_with_organizer} <- save_physical_address(args_with_organizer),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
MobilizonWeb.API.Events.create_event(args_with_organizer) do
{:ok, event}
@@ -309,8 +298,6 @@ defmodule MobilizonWeb.Resolvers.Event do
{:is_owned, %Actor{} = organizer_actor} <-
User.owns_actor(user, event.organizer_actor_id),
args <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, args} <- save_attached_picture(args),
{:ok, args} <- save_physical_address(args),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
MobilizonWeb.API.Events.update_event(args, event) do
{:ok, event}
@@ -327,47 +314,6 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, "You need to be logged-in to update an event"}
end
# If we have an attached picture, just transmit it. It will be handled by
# Mobilizon.Service.ActivityPub.Utils.make_picture_data/1
# However, we need to pass its actor ID
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(
%{picture: %{picture: %{file: %Plug.Upload{} = _picture} = all_pic}} = args
) do
{:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor.id))}
end
# Otherwise if we use a previously uploaded picture we need to fetch it from database
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(%{picture: %{picture_id: picture_id}} = args) do
with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do
{:ok, Map.put(args, :picture, picture)}
end
end
@spec save_attached_picture(map()) :: {:ok, map()}
defp save_attached_picture(args), do: {:ok, args}
@spec save_physical_address(map()) :: {:ok, map()}
defp save_physical_address(%{physical_address: %{url: physical_address_url}} = args)
when not is_nil(physical_address_url) do
with %Address{} = address <- Addresses.get_address_by_url(physical_address_url),
args <- Map.put(args, :physical_address, address.url) do
{:ok, args}
end
end
@spec save_physical_address(map()) :: {:ok, map()}
defp save_physical_address(%{physical_address: address} = args) when address != nil do
with {:ok, %Address{} = address} <- Addresses.create_address(address),
args <- Map.put(args, :physical_address, address.url) do
{:ok, args}
end
end
@spec save_physical_address(map()) :: {:ok, map()}
defp save_physical_address(args), do: {:ok, args}
@doc """
Delete an event
"""

View File

@@ -6,7 +6,6 @@ defmodule MobilizonWeb.Resolvers.Group do
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.Activity
alias Mobilizon.Users.User
alias MobilizonWeb.API
@@ -47,23 +46,18 @@ defmodule MobilizonWeb.Resolvers.Group do
args,
%{context: %{current_user: user}}
) do
with {
:ok,
%Activity{data: %{"object" => %{"type" => "Group"} = _object}},
%Actor{} = group
} <-
API.Groups.create_group(
user,
%{
preferred_username: args.preferred_username,
creator_actor_id: args.creator_actor_id,
name: Map.get(args, "name", args.preferred_username),
summary: args.summary,
avatar: Map.get(args, "avatar"),
banner: Map.get(args, "banner")
}
) do
with creator_actor_id <- Map.get(args, :creator_actor_id),
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, creator_actor_id),
args <- Map.put(args, :creator_actor, actor),
{:ok, _activity, %Actor{type: :Group} = group} <-
API.Groups.create_group(args) do
{:ok, group}
else
{:error, err} when is_bitstring(err) ->
{:error, err}
{:is_owned, nil} ->
{:error, "Creator actor id is not owned by the current user"}
end
end

View File

@@ -155,7 +155,7 @@ defmodule MobilizonWeb.Resolvers.Person do
if Map.has_key?(args, key) && !is_nil(args[key][:picture]) do
pic = args[key][:picture]
with {:ok, %{"name" => name, "url" => [%{"href" => url, "mediaType" => content_type}]}} <-
with {:ok, %{name: name, url: url, content_type: content_type, size: _size}} <-
MobilizonWeb.Upload.store(pic.file, type: key, description: pic.alt) do
Map.put(args, key, %{"name" => name, "url" => url, "mediaType" => content_type})
end

View File

@@ -51,7 +51,7 @@ defmodule MobilizonWeb.Resolvers.Picture do
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, %{"url" => [%{"href" => url, "mediaType" => content_type}], "size" => size}} <-
{:ok, %{name: _name, url: url, content_type: content_type, size: size}} <-
MobilizonWeb.Upload.store(file),
args <-
args

View File

@@ -35,7 +35,7 @@ defmodule MobilizonWeb.Schema.CommentType do
@desc "Create a comment"
field :create_comment, type: :comment do
arg(:text, non_null(:string))
arg(:actor_username, non_null(:string))
arg(:actor_id, non_null(:id))
resolve(&Comment.create_comment/3)
end

View File

@@ -63,7 +63,7 @@ defmodule MobilizonWeb.Schema.EventType do
field(:draft, :boolean, description: "Whether or not the event is a draft")
field(:participant_stats, :participant_stats, resolve: &Event.stats_participants_for_event/3)
field(:participant_stats, :participant_stats)
field(:participants, list_of(:participant), description: "The event's participants") do
arg(:page, :integer, default_value: 1)
@@ -112,13 +112,21 @@ defmodule MobilizonWeb.Schema.EventType do
end
object :participant_stats do
field(:approved, :integer, description: "The number of approved participants")
field(:unapproved, :integer, description: "The number of unapproved participants")
field(:going, :integer,
description: "The number of approved participants",
resolve: &Event.stats_participants_going/3
)
field(:not_approved, :integer, description: "The number of not approved participants")
field(:rejected, :integer, description: "The number of rejected participants")
field(:participants, :integer,
field(:participant, :integer,
description: "The number of simple participants (excluding creators)"
)
field(:moderator, :integer, description: "The number of moderators")
field(:administrator, :integer, description: "The number of administrators")
field(:creator, :integer, description: "The number of creators")
end
object :event_offer do

View File

@@ -73,16 +73,10 @@ defmodule MobilizonWeb.Upload do
{:ok, url_spec} <- Uploaders.Uploader.put_file(opts.uploader, upload) do
{:ok,
%{
"type" => opts.activity_type || get_type(upload.content_type),
"url" => [
%{
"type" => "Link",
"mediaType" => upload.content_type,
"href" => url_from_spec(upload, opts.base_url, url_spec)
}
],
"size" => upload.size,
"name" => Map.get(opts, :description) || upload.name
name: Map.get(opts, :description) || upload.name,
url: url_from_spec(upload, opts.base_url, url_spec),
content_type: upload.content_type,
size: upload.size
}}
else
{:error, error} ->
@@ -166,16 +160,6 @@ defmodule MobilizonWeb.Upload do
defp check_file_size(_, _), do: :ok
@picture_content_types ["image/gif", "image/png", "image/jpg", "image/jpeg", "image/webp"]
# Return whether the upload is a picture or not
defp get_type(content_type) do
if content_type in @picture_content_types do
"Image"
else
"Document"
end
end
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path =
URI.encode(path, &char_unescaped?/1) <>

View File

@@ -8,7 +8,8 @@ defmodule Mobilizon.Service.ActivityPub do
# ActivityPub context.
"""
import Mobilizon.Service.ActivityPub.{Utils, Visibility}
import Mobilizon.Service.ActivityPub.Utils
import Mobilizon.Service.ActivityPub.Visibility
alias Mobilizon.{Actors, Config, Events}
alias Mobilizon.Actors.{Actor, Follower}
@@ -16,17 +17,12 @@ defmodule Mobilizon.Service.ActivityPub do
alias Mobilizon.Service.ActivityPub.{Activity, Converter, Convertible, Relay, Transmogrifier}
alias Mobilizon.Service.{Federator, WebFinger}
alias Mobilizon.Service.HTTPSignatures.Signature
alias MobilizonWeb.API.Utils, as: APIUtils
alias Mobilizon.Service.ActivityPub.Audience
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
require Logger
@doc """
Get recipients for an activity or object
"""
@spec get_recipients(map()) :: list()
def get_recipients(data) do
(data["to"] || []) ++ (data["cc"] || [])
end
@doc """
Wraps an object into an activity
"""
@@ -106,8 +102,8 @@ defmodule Mobilizon.Service.ActivityPub do
@doc """
Getting an actor from url, eventually creating it
"""
@spec get_or_fetch_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
def get_or_fetch_by_url(url, preload \\ false) do
@spec get_or_fetch_actor_by_url(String.t(), boolean) :: {:ok, Actor.t()} | {:error, String.t()}
def get_or_fetch_actor_by_url(url, preload \\ false) do
case Actors.get_actor_by_url(url, preload) do
{:ok, %Actor{} = actor} ->
{:ok, actor}
@@ -126,27 +122,29 @@ defmodule Mobilizon.Service.ActivityPub do
end
@doc """
Create an activity of type "Create"
"""
def create(%{to: to, actor: actor, object: object} = params) do
Logger.debug("creating an activity")
Logger.debug(inspect(params))
Logger.debug(inspect(object))
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]
Create an activity of type `Create`
with create_data <-
make_create_data(
%{to: to, actor: actor, published: published, object: object},
additional
),
{:ok, activity} <- create_activity(create_data, local),
{:ok, object} <- insert_full_object(create_data),
* Creates the object, which returns AS data
* Wraps ActivityStreams data into a `Create` activity
* Creates an `Mobilizon.Service.ActivityPub.Activity` from this
* Federates (asynchronously) the activity
* Returns the activity
"""
@spec create(atom(), map(), boolean, map()) :: {:ok, Activity.t(), struct()} | any()
def create(type, args, local \\ false, additional \\ %{}) do
Logger.debug("creating an activity")
Logger.debug(inspect(args))
{:ok, entity, create_data} =
case type do
:event -> create_event(args, additional)
:comment -> create_comment(args, additional)
:group -> create_group(args, additional)
end
with {:ok, activity} <- create_activity(create_data, local),
:ok <- maybe_federate(activity) do
# {:ok, actor} <- Actors.increase_event_count(actor) do
{:ok, activity, object}
{:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating an activity")
@@ -155,21 +153,52 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
def accept(%{to: to, actor: actor, object: object} = params, activity_wrapper_id \\ nil) do
# only accept false as false value
local = !(params[:local] == false)
@doc """
Create an activity of type `Update`
with data <- %{
"to" => to,
"type" => "Accept",
"actor" => actor,
"object" => object,
"id" => activity_wrapper_id || get_url(object) <> "/activity"
},
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
* Updates the object, which returns AS data
* Wraps ActivityStreams data into a `Update` activity
* Creates an `Mobilizon.Service.ActivityPub.Activity` from this
* 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
Logger.debug("updating an activity")
Logger.debug(inspect(args))
{:ok, entity, update_data} =
case type do
:event -> update_event(old_entity, args, additional)
:actor -> update_actor(old_entity, args, additional)
end
with {:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
{:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
end
end
def accept(type, entity, args, local \\ false, additional \\ %{}) do
{:ok, entity, update_data} =
case type do
:join -> accept_join(entity, args, additional)
:follow -> accept_follow(entity, args, additional)
end
with {:ok, activity} <- create_activity(update_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, entity}
else
err ->
Logger.error("Something went wrong while creating an activity")
Logger.debug(inspect(err))
err
end
end
@@ -191,25 +220,6 @@ defmodule Mobilizon.Service.ActivityPub do
end
end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{
"to" => to,
"cc" => cc,
"id" => object["url"],
"type" => "Update",
"actor" => actor,
"object" => object
},
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- update_object(object["id"], data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
end
end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
# def like(
# %Actor{url: url} = actor,
@@ -290,15 +300,12 @@ defmodule Mobilizon.Service.ActivityPub do
Make an actor follow another
"""
def follow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{url: follow_url}} <-
with {:ok, %Follower{} = follower} <-
Actors.follow(followed, follower, activity_id, false),
activity_follow_id <-
activity_id || follow_url,
data <- make_follow_data(followed, follower, activity_follow_id),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
follower_as_data <- Convertible.model_to_as(follower),
{:ok, activity} <- create_activity(follower_as_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
{:ok, activity, follower}
else
{:error, err, msg} when err in [:already_following, :suspended] ->
{:error, msg}
@@ -310,16 +317,11 @@ defmodule Mobilizon.Service.ActivityPub do
"""
@spec unfollow(Actor.t(), Actor.t(), String.t(), boolean()) :: {:ok, map()} | any()
def unfollow(%Actor{} = follower, %Actor{} = followed, activity_id \\ nil, local \\ true) do
with {:ok, %Follower{id: follow_id}} <- Actors.unfollow(followed, follower),
with {:ok, %Follower{id: follow_id} = follow} <- Actors.unfollow(followed, follower),
# We recreate the follow activity
data <-
make_follow_data(
followed,
follower,
"#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity"
),
{:ok, follow_activity} <- create_activity(data, local),
{:ok, _object} <- insert_full_object(data),
follow_as_data <-
Convertible.model_to_as(%{follow | actor: follower, target_actor: followed}),
{:ok, follow_activity} <- create_activity(follow_as_data, local),
activity_unfollow_id <-
activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity",
unfollow_data <-
@@ -346,7 +348,7 @@ defmodule Mobilizon.Service.ActivityPub do
"id" => url <> "/delete"
}
with {:ok, _} <- Events.delete_event(event),
with {:ok, %Event{} = event} <- Events.delete_event(event),
{:ok, activity} <- create_activity(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity, event}
@@ -362,11 +364,10 @@ defmodule Mobilizon.Service.ActivityPub do
"to" => [actor.url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
}
with {:ok, _} <- Events.delete_comment(comment),
with {:ok, %Comment{} = comment} <- Events.delete_comment(comment),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
{:ok, activity, comment}
end
end
@@ -379,11 +380,10 @@ defmodule Mobilizon.Service.ActivityPub do
"to" => [url <> "/followers", "https://www.w3.org/ns/activitystreams#Public"]
}
with {:ok, _} <- Actors.delete_actor(actor),
with {:ok, %Actor{} = actor} <- Actors.delete_actor(actor),
{:ok, activity} <- create_activity(data, local),
{:ok, object} <- insert_full_object(data),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
{:ok, activity, actor}
end
end
@@ -434,9 +434,9 @@ defmodule Mobilizon.Service.ActivityPub do
{:ok, _object} <- insert_full_object(join_data),
:ok <- maybe_federate(activity) do
if role === :participant do
accept(
%{to: [actor.url], actor: event.organizer_actor.url, object: join_data["id"]},
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{participant.id}"
accept_join(
participant,
%{}
)
end
@@ -720,9 +720,233 @@ defmodule Mobilizon.Service.ActivityPub do
}
end
# # Whether the Public audience is in the activity's audience
# defp is_public?(activity) do
# "https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
# (activity.data["cc"] || []))
# end
# Get recipients for an activity or object
@spec get_recipients(map()) :: list()
defp get_recipients(data) do
(data["to"] || []) ++ (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.organizer_actor,
args.mentions,
nil,
event.visibility
),
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} <- Events.create_comment(args),
comment_as_data <- Convertible.model_to_as(comment),
audience <-
Audience.calculate_to_and_cc_from_mentions(
comment.actor,
args.mentions,
args.in_reply_to_comment,
comment.visibility
),
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 <-
Audience.calculate_to_and_cc_from_mentions(
args.creator_actor,
[],
nil,
:public
),
create_data <-
make_create_data(group_as_data, Map.merge(audience, additional)) do
{:ok, group, create_data}
end
end
@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),
event_as_data <- Convertible.model_to_as(new_event),
audience <-
Audience.calculate_to_and_cc_from_mentions(
new_event.organizer_actor,
Map.get(args, :mentions, []),
nil,
new_event.visibility
),
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_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),
audience <-
Audience.calculate_to_and_cc_from_mentions(
new_actor,
[],
nil,
:public
),
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 accept_follow(Follower.t(), map(), map()) ::
{:ok, Follower.t(), Activity.t()} | any()
defp accept_follow(
%Follower{} = follower,
args,
additional
) do
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, args),
follower_as_data <- Convertible.model_to_as(follower),
audience <-
Audience.calculate_to_and_cc_from_mentions(follower.target_actor),
update_data <-
make_update_data(
follower_as_data,
Map.merge(Map.merge(audience, additional), %{
"id" => "#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follower.id}"
})
) do
{:ok, follower, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
err
end
end
@spec accept_join(Participant.t(), map(), map()) ::
{:ok, Participant.t(), Activity.t()} | any()
defp accept_join(
%Participant{} = participant,
args,
additional \\ %{}
) do
with {:ok, %Participant{} = participant} <- Events.update_participant(participant, args),
participant_as_data <- Convertible.model_to_as(participant),
audience <-
Audience.calculate_to_and_cc_from_mentions(participant.actor),
update_data <-
make_accept_join_data(
participant_as_data,
Map.merge(Map.merge(audience, additional), %{
"id" => "#{MobilizonWeb.Endpoint.url()}/accept/join/#{participant.id}"
})
) do
{:ok, participant, update_data}
else
err ->
Logger.error("Something went wrong while creating an update activity")
Logger.debug(inspect(err))
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(HtmlSanitizeEx.strip_tags(&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
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) |> Events.get_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,
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))
}) do
args
end
end
defp prepare_args_for_group(args) do
with preferred_username <-
args |> Map.get(:preferred_username) |> HtmlSanitizeEx.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

View File

@@ -0,0 +1,98 @@
defmodule Mobilizon.Service.ActivityPub.Audience do
@moduledoc """
Tools for calculating content audience
"""
alias Mobilizon.Actors.Actor
@ap_public "https://www.w3.org/ns/activitystreams#Public"
@doc """
Determines the full audience based on mentions for a public audience
Audience is:
* `to` : the mentioned actors, the eventual actor we're replying to and the public
* `cc` : the actor's followers
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :public) do
to = [@ap_public | mentions]
cc = [actor.followers_url]
if in_reply_to do
{Enum.uniq([in_reply_to.actor | to]), cc}
else
{to, cc}
end
end
@doc """
Determines the full audience based on mentions based on a unlisted audience
Audience is:
* `to` : the mentionned actors, actor's followers and the eventual actor we're replying to
* `cc` : public
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :unlisted) do
to = [actor.followers_url | mentions]
cc = [@ap_public]
if in_reply_to do
{Enum.uniq([in_reply_to.actor | to]), cc}
else
{to, cc}
end
end
@doc """
Determines the full audience based on mentions based on a private audience
Audience is:
* `to` : the mentioned actors, actor's followers and the eventual actor we're replying to
* `cc` : none
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(%Actor{} = actor, mentions, in_reply_to, :private) do
{to, cc} = get_to_and_cc(actor, mentions, in_reply_to, :direct)
{[actor.followers_url | to], cc}
end
@doc """
Determines the full audience based on mentions based on a direct audience
Audience is:
* `to` : the mentioned actors and the eventual actor we're replying to
* `cc` : none
"""
@spec get_to_and_cc(Actor.t(), list(), map(), String.t()) :: {list(), list()}
def get_to_and_cc(_actor, mentions, in_reply_to, :direct) do
if in_reply_to do
{Enum.uniq([in_reply_to.actor | mentions]), []}
else
{mentions, []}
end
end
def get_to_and_cc(_actor, mentions, _in_reply_to, {:list, _}) do
{mentions, []}
end
# def get_addressed_actors(_, to) when is_list(to) do
# Actors.get(to)
# end
def get_addressed_actors(mentioned_users, _), do: mentioned_users
def calculate_to_and_cc_from_mentions(
actor,
mentions \\ [],
in_reply_to \\ nil,
visibility \\ :public
) do
with mentioned_actors <- for({_, mentioned_actor} <- mentions, do: mentioned_actor.url),
addressed_actors <- get_addressed_actors(mentioned_actors, nil),
{to, cc} <- get_to_and_cc(actor, addressed_actors, in_reply_to, visibility) do
%{"to" => to, "cc" => cc}
end
end
end

View File

@@ -37,17 +37,18 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Actor do
"url" => object["image"]["url"]
}
%{
"type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferredUsername"],
"summary" => object["summary"],
"url" => object["id"],
"name" => object["name"],
"avatar" => avatar,
"banner" => banner,
"keys" => object["publicKey"]["publicKeyPem"],
"manually_approves_followers" => object["manuallyApprovesFollowers"]
}
{:ok,
%{
"type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferredUsername"],
"summary" => object["summary"],
"url" => object["id"],
"name" => object["name"],
"avatar" => avatar,
"banner" => banner,
"keys" => object["publicKey"]["publicKeyPem"],
"manually_approves_followers" => object["manuallyApprovesFollowers"]
}}
end
@doc """

View File

@@ -11,6 +11,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible}
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
require Logger
@@ -26,57 +27,67 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
Converts an AP object data to our internal data structure.
"""
@impl Converter
@spec as_to_model_data(map) :: map
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
def as_to_model_data(object) do
{:ok, %Actor{id: actor_id}} = ActivityPub.get_or_fetch_by_url(object["actor"])
Logger.debug("Inserting full comment")
Logger.debug("We're converting raw ActivityStream data to a comment entity")
Logger.debug(inspect(object))
data = %{
"text" => object["content"],
"url" => object["id"],
"actor_id" => actor_id,
"in_reply_to_comment_id" => nil,
"event_id" => nil,
"uuid" => object["uuid"]
}
with {:ok, %Actor{id: actor_id}} <- ActivityPub.get_or_fetch_actor_by_url(object["actor"]),
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])},
{:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(object["tag"])} do
Logger.debug("Inserting full comment")
Logger.debug(inspect(object))
# We fetch the parent object
Logger.debug("We're fetching the parent object")
data = %{
text: object["content"],
url: object["id"],
actor_id: actor_id,
in_reply_to_comment_id: nil,
event_id: nil,
uuid: object["uuid"],
tags: tags,
mentions: mentions
}
data =
if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
object["inReplyTo"] != "" do
Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
# We fetch the parent object
Logger.debug("We're fetching the parent object")
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)
data =
if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
object["inReplyTo"] != "" do
Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
# Reply to a comment (Comment)
{:ok, %CommentModel{id: id} = comment} ->
Logger.debug("Parent object is another comment")
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)
data
|> Map.put("in_reply_to_comment_id", id)
|> Map.put("origin_comment_id", comment |> CommentModel.get_thread_id())
# Reply to a comment (Comment)
{:ok, %CommentModel{id: id} = comment} ->
Logger.debug("Parent object is another comment")
# Anything else is kind of a MP
{:error, parent} ->
Logger.debug("Parent object is something we don't handle")
Logger.debug(inspect(parent))
data
data
|> Map.put(:in_reply_to_comment_id, id)
|> Map.put(:origin_comment_id, comment |> CommentModel.get_thread_id())
# Anything else is kind of a MP
{:error, parent} ->
Logger.debug("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
else
Logger.debug("No parent object for this comment")
data
end
data
{:ok, data}
else
err ->
{:error, err}
end
end
@doc """
@@ -85,14 +96,22 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
@impl Converter
@spec model_to_as(CommentModel.t()) :: map
def model_to_as(%CommentModel{} = comment) do
to =
if comment.visibility == :public,
do: ["https://www.w3.org/ns/activitystreams#Public"],
else: [comment.actor.followers_url]
object = %{
"type" => "Note",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"to" => to,
"cc" => [],
"content" => comment.text,
"actor" => comment.actor.url,
"attributedTo" => comment.actor.url,
"uuid" => comment.uuid,
"id" => comment.url
"id" => comment.url,
"tag" =>
ConverterUtils.build_mentions(comment.mentions) ++ ConverterUtils.build_tags(comment.tags)
}
if comment.in_reply_to_comment do

View File

@@ -6,15 +6,17 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
internal one, and back.
"""
alias Mobilizon.{Actors, Addresses, Events, Media}
alias Mobilizon.{Addresses, Media}
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Events.{EventOptions, Tag}
alias Mobilizon.Events.EventOptions
alias Mobilizon.Media.Picture
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils}
alias Mobilizon.Service.ActivityPub.Converter.Address, as: AddressConverter
alias Mobilizon.Service.ActivityPub.Converter.Picture, as: PictureConverter
alias Mobilizon.Service.ActivityPub.Converter.Utils, as: ConverterUtils
require Logger
@@ -30,16 +32,16 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
Converts an AP object data to our internal data structure.
"""
@impl Converter
@spec as_to_model_data(map) :: map
@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 {:actor, {:ok, %Actor{id: actor_id}}} <-
{:actor, Actors.get_actor_by_url(object["actor"])},
{:actor, ActivityPub.get_or_fetch_actor_by_url(object["actor"])},
{:address, address_id} <-
{:address, get_address(object["location"])},
{:tags, tags} <- {:tags, fetch_tags(object["tag"])},
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])},
{:visibility, visibility} <- {:visibility, get_visibility(object)},
{:options, options} <- {:options, get_options(object)} do
picture_id =
@@ -58,26 +60,27 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
end
entity = %{
"title" => object["name"],
"description" => object["content"],
"organizer_actor_id" => actor_id,
"picture_id" => picture_id,
"begins_on" => object["startTime"],
"ends_on" => object["endTime"],
"category" => object["category"],
"visibility" => visibility,
"join_options" => object["joinOptions"],
"status" => object["status"],
"online_address" => object["onlineAddress"],
"phone_address" => object["phoneAddress"],
"draft" => object["draft"] || false,
"url" => object["id"],
"uuid" => object["uuid"],
"tags" => tags,
"physical_address_id" => address_id
title: object["name"],
description: object["content"],
organizer_actor_id: actor_id,
picture_id: picture_id,
begins_on: object["startTime"],
ends_on: object["endTime"],
category: object["category"],
visibility: visibility,
join_options: Map.get(object, "joinOptions", "free"),
options: options,
status: object["status"],
online_address: object["onlineAddress"],
phone_address: object["phoneAddress"],
draft: object["draft"] || false,
url: object["id"],
uuid: object["uuid"],
tags: tags,
physical_address_id: address_id
}
{:ok, Map.put(entity, "options", options)}
{:ok, entity}
else
error ->
{:error, error}
@@ -111,7 +114,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
"startTime" => event.begins_on |> date_to_string(),
"joinOptions" => to_string(event.join_options),
"endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> build_tags(),
"tag" => event.tags |> ConverterUtils.build_tags(),
"draft" => event.draft,
"id" => event.url,
"url" => event.url
@@ -181,32 +184,6 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
end
end
@spec fetch_tags([String.t()]) :: [String.t()]
defp fetch_tags(tags) do
Logger.debug("fetching tags")
Enum.reduce(tags, [], fn tag, acc ->
with true <- tag["type"] == "Hashtag",
{:ok, %Tag{} = tag} <- Events.get_or_create_tag(tag) do
acc ++ [tag]
else
_err ->
acc
end
end)
end
@spec build_tags([String.t()]) :: String.t()
defp build_tags(tags) do
Enum.map(tags, fn %Tag{} = tag ->
%{
"href" => MobilizonWeb.Endpoint.url() <> "/tags/#{tag.slug}",
"name" => "##{tag.title}",
"type" => "Hashtag"
}
end)
end
@ap_public "https://www.w3.org/ns/activitystreams#Public"
defp get_visibility(object) do

View File

@@ -0,0 +1,36 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Follower do
@moduledoc """
Participant converter.
This module allows to convert followers from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.Actors.Follower, as: FollowerModel
alias Mobilizon.Service.ActivityPub.Convertible
alias Mobilizon.Actors.Actor
defimpl Convertible, for: FollowerModel do
alias Mobilizon.Service.ActivityPub.Converter.Follower, as: FollowerConverter
defdelegate model_to_as(follower), to: FollowerConverter
end
@doc """
Convert an follow struct to an ActivityStream representation.
"""
@spec model_to_as(FollowerModel.t()) :: map
def model_to_as(
%FollowerModel{actor: %Actor{} = actor, target_actor: %Actor{} = target_actor} = follower
) do
%{
"type" => "Follow",
"actor" => actor.url,
"to" => [target_actor.url],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => target_actor.url,
"id" => follower.url
}
end
end

View File

@@ -0,0 +1,100 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Utils do
@moduledoc """
Various utils for converters
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Tag
alias Mobilizon.Mention
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Storage.Repo
require Logger
@spec fetch_tags([String.t()]) :: [Tag.t()]
def fetch_tags(tags) when is_list(tags) do
Logger.debug("fetching tags")
Enum.reduce(tags, [], &fetch_tag/2)
end
@spec fetch_mentions([map()]) :: [map()]
def fetch_mentions(mentions) when is_list(mentions) do
Logger.debug("fetching mentions")
Enum.reduce(mentions, [], fn mention, acc -> create_mention(mention, acc) end)
end
def fetch_address(%{id: id}) do
with {id, ""} <- Integer.parse(id) do
%{id: id}
end
end
def fetch_address(address) when is_map(address) do
address
end
@spec build_tags([Tag.t()]) :: [Map.t()]
def build_tags(tags) do
Enum.map(tags, fn %Tag{} = tag ->
%{
"href" => MobilizonWeb.Endpoint.url() <> "/tags/#{tag.slug}",
"name" => "##{tag.title}",
"type" => "Hashtag"
}
end)
end
def build_mentions(mentions) do
Enum.map(mentions, fn %Mention{} = mention ->
if Ecto.assoc_loaded?(mention.actor) do
build_mention(mention.actor)
else
build_mention(Repo.preload(mention, [:actor]).actor)
end
end)
end
defp build_mention(%Actor{} = actor) do
%{
"href" => actor.url,
"name" => "@#{Mobilizon.Actors.Actor.preferred_username_and_domain(actor)}",
"type" => "Mention"
}
end
defp fetch_tag(tag, acc) when is_map(tag) do
case tag["type"] do
"Hashtag" ->
acc ++ [%{title: tag}]
_err ->
acc
end
end
defp fetch_tag(tag, acc) when is_bitstring(tag) do
acc ++ [%{title: tag}]
end
@spec create_mention(map(), list()) :: list()
defp create_mention(%Actor{id: actor_id} = _mention, acc) do
acc ++ [%{actor_id: actor_id}]
end
@spec create_mention(map(), list()) :: list()
defp create_mention(mention, acc) when is_map(mention) do
with true <- mention["type"] == "Mention",
{:ok, %Actor{id: actor_id}} <- ActivityPub.get_or_fetch_actor_by_url(mention["href"]) do
acc ++ [%{actor_id: actor_id}]
else
_err ->
acc
end
end
@spec create_mention({String.t(), map()}, list()) :: list()
defp create_mention({_, mention}, acc) when is_map(mention) do
create_mention(mention, acc)
end
end

View File

@@ -26,7 +26,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def follow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance),
{:ok, activity} <- Follows.follow(local_actor, target_actor) do
Logger.info("Relay: followed instance #{target_instance}; id=#{activity.data["id"]}")
{:ok, activity}
@@ -39,7 +39,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def unfollow(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance),
{:ok, activity} <- Follows.unfollow(local_actor, target_actor) do
Logger.info("Relay: unfollowed instance #{target_instance}: id=#{activity.data["id"]}")
{:ok, activity}
@@ -52,7 +52,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
def accept(target_instance) do
with %Actor{} = local_actor <- get_actor(),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_by_url(target_instance),
{:ok, %Actor{} = target_actor} <- ActivityPub.get_or_fetch_actor_by_url(target_instance),
{:ok, activity} <- Follows.accept(target_actor, local_actor) do
{:ok, activity}
end
@@ -60,7 +60,7 @@ defmodule Mobilizon.Service.ActivityPub.Relay do
# def reject(target_instance) do
# with %Actor{} = local_actor <- get_actor(),
# {:ok, %Actor{} = target_actor} <- Activity.get_or_fetch_by_url(target_instance),
# {:ok, %Actor{} = target_actor} <- Activity.get_or_fetch_actor_by_url(target_instance),
# {:ok, activity} <- Follows.reject(target_actor, local_actor) do
# {:ok, activity}
# end

View File

@@ -13,9 +13,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
alias Mobilizon.Events
alias Mobilizon.Events.{Comment, Event, Participant}
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils, Visibility}
alias Mobilizon.Service.ActivityPub.{Activity, Converter, Convertible, Utils}
alias MobilizonWeb.Email.Participation
import Mobilizon.Service.ActivityPub.Utils
require Logger
def get_actor(%{"actor" => actor}) when is_binary(actor) do
@@ -138,59 +140,65 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
@doc """
Handles a `Create` activity for `Note` (comments) objects
The following actions are performed
* Fetch the author of the activity
* Convert the ActivityStream data to the comment model format (it also finds and inserts tags)
* Get (by it's URL) or create the comment with this data
* Insert eventual mentions in the database
* Convert the comment back in ActivityStreams data
* Wrap this data back into a `Create` activity
* Return the activity and the comment object
"""
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = object}) do
Logger.info("Handle incoming to create notes")
with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(data["actor"]) do
Logger.debug("found actor")
Logger.debug(inspect(actor))
params = %{
to: data["to"],
object: object |> fix_object,
actor: actor,
local: false,
published: data["published"],
additional:
Map.take(data, [
"cc",
"id"
])
}
ActivityPub.create(params)
with {:ok, object_data} <-
object |> fix_object() |> Converter.Comment.as_to_model_data(),
{:existing_comment, {:error, :comment_not_found}} <-
{:existing_comment, Events.get_comment_from_url_with_preload(object_data.url)},
{:ok, %Activity{} = activity, %Comment{} = comment} <-
ActivityPub.create(:comment, object_data, false) do
{:ok, activity, comment}
else
{:existing_comment, {:ok, %Comment{} = comment}} ->
{:ok, nil, comment}
end
end
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object} = data) do
@doc """
Handles a `Create` activity for `Event` objects
The following actions are performed
* Fetch the author of the activity
* Convert the ActivityStream data to the event model format (it also finds and inserts tags)
* Get (by it's URL) or create the event with this data
* Insert eventual mentions in the database
* Convert the event back in ActivityStreams data
* Wrap this data back into a `Create` activity
* Return the activity and the event object
"""
def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Event"} = object}) do
Logger.info("Handle incoming to create event")
with {:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(data["actor"]) do
Logger.debug("found actor")
Logger.debug(inspect(actor))
params = %{
to: data["to"],
object: object |> fix_object,
actor: actor,
local: false,
published: data["published"],
additional:
Map.take(data, [
"cc",
"id"
])
}
ActivityPub.create(params)
with {:ok, object_data} <-
object |> fix_object() |> Converter.Event.as_to_model_data(),
{:existing_event, nil} <- {:existing_event, Events.get_event_by_url(object_data.url)},
{:ok, %Activity{} = activity, %Event{} = event} <-
ActivityPub.create(:event, object_data, false) do
{:ok, activity, event}
else
{:existing_event, %Event{} = event} -> {:ok, nil, event}
end
end
def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = _data
) do
with {:ok, %Actor{} = followed} <- ActivityPub.get_or_fetch_by_url(followed, true),
{:ok, %Actor{} = follower} <- ActivityPub.get_or_fetch_by_url(follower),
with {:ok, %Actor{} = followed} <- ActivityPub.get_or_fetch_actor_by_url(followed, true),
{:ok, %Actor{} = follower} <- ActivityPub.get_or_fetch_actor_by_url(follower),
{:ok, activity, object} <- ActivityPub.follow(follower, followed, id, false) do
{:ok, activity, object}
else
@@ -209,8 +217,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
} = data
) do
with actor_url <- get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor_url),
{:object_not_found, {:ok, activity, object}} <-
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
{:object_not_found, {:ok, %Activity{} = activity, object}} <-
{:object_not_found,
do_handle_incoming_accept_following(accepted_object, actor) ||
do_handle_incoming_accept_join(accepted_object, actor)} do
@@ -238,7 +246,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
%{"type" => "Reject", "object" => rejected_object, "actor" => _actor, "id" => id} = data
) do
with actor_url <- get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor_url),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
{:object_not_found, {:ok, activity, object}} <-
{:object_not_found,
do_handle_incoming_reject_following(rejected_object, actor) ||
@@ -278,13 +286,20 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# end
# #
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => _id} = data
) do
with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor),
# TODO: Is the following line useful?
{:ok, %Actor{} = _actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
:ok <- Logger.debug("Fetching contained object"),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
public <- Visibility.is_public?(data),
{:ok, activity, object} <- ActivityPub.announce(actor, object, id, false, public) do
:ok <- Logger.debug("Handling contained object"),
create_data <-
make_create_data(object),
:ok <- Logger.debug(inspect(object)),
{:ok, _activity, object} <- handle_incoming(create_data),
:ok <- Logger.debug("Finished processing contained object"),
{:ok, activity} <- ActivityPub.create_activity(data, false) do
{:ok, activity, object}
else
e ->
@@ -293,21 +308,19 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} =
data
)
def handle_incoming(%{
"type" => "Update",
"object" => %{"type" => object_type} = object,
"actor" => _actor_id
})
when object_type in ["Person", "Group", "Application", "Service", "Organization"] do
case Actors.get_actor_by_url(object["id"]) do
{:ok, %Actor{url: actor_url}} ->
ActivityPub.update(%{
local: false,
to: data["to"] || [],
cc: data["cc"] || [],
object: object,
actor: actor_url
})
with {:ok, %Actor{} = old_actor} <- Actors.get_actor_by_url(object["id"]),
{:ok, object_data} <-
object |> fix_object() |> Converter.Actor.as_to_model_data(),
{:ok, %Activity{} = activity, %Actor{} = new_actor} <-
ActivityPub.update(:actor, old_actor, object_data, false) do
{:ok, activity, new_actor}
else
e ->
Logger.debug(inspect(e))
:error
@@ -315,24 +328,19 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => actor} =
%{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => _actor} =
_update
) do
with {:ok, %{"actor" => existing_organizer_actor_url} = existing_event_data} <-
fetch_obj_helper_as_activity_streams(object),
object <- Map.merge(existing_event_data, object),
{:ok, %Actor{url: actor_url}} <- actor |> Utils.get_url() |> Actors.get_actor_by_url(),
true <- Utils.get_url(existing_organizer_actor_url) == actor_url do
ActivityPub.update(%{
local: false,
to: object["to"] || [],
cc: object["cc"] || [],
object: object,
actor: actor_url
})
with %Event{} = old_event <-
Events.get_event_by_url(object["id"]),
{:ok, object_data} <-
object |> fix_object() |> Converter.Event.as_to_model_data(),
{:ok, %Activity{} = activity, %Event{} = new_event} <-
ActivityPub.update(:event, old_event, object_data, false) do
{:ok, activity, new_event}
else
e ->
Logger.debug(inspect(e))
Logger.error(inspect(e))
:error
end
end
@@ -350,7 +358,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
} = data
) do
with actor <- get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_by_url(actor),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor),
{:ok, object} <- fetch_obj_helper_as_activity_streams(object_id),
{:ok, activity, object} <-
ActivityPub.unannounce(actor, object, id, cancelled_activity_id, false) do
@@ -454,7 +462,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# } = data
# ) do
# with actor <- get_actor(data),
# %Actor{} = actor <- ActivityPub.get_or_fetch_by_url(actor),
# %Actor{} = actor <- ActivityPub.get_or_fetch_actor_by_url(actor),
# {:ok, object} <- fetch_obj_helper(object_id) || fetch_obj_helper(object_id),
# {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
# {:ok, activity}
@@ -472,23 +480,16 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
Handle incoming `Accept` activities wrapping a `Follow` activity
"""
def do_handle_incoming_accept_following(follow_object, %Actor{} = actor) do
with {:follow,
{:ok,
%Follower{approved: false, actor: follower, id: follow_id, target_actor: followed} =
follow}} <-
with {:follow, {:ok, %Follower{approved: false, target_actor: followed} = follow}} <-
{:follow, get_follow(follow_object)},
{:same_actor, true} <- {:same_actor, actor.id == followed.id},
{:ok, activity, _} <-
{:ok, %Activity{} = activity, %Follower{approved: true} = follow} <-
ActivityPub.accept(
%{
to: [follower.url],
actor: actor.url,
object: follow_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/accept/follow/#{follow_id}"
),
{:ok, %Follower{approved: true}} <- Actors.update_follower(follow, %{"approved" => true}) do
:follow,
follow,
%{approved: true},
false
) do
{:ok, activity, follow}
else
{:follow, _} ->
@@ -546,26 +547,18 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
# Handle incoming `Accept` activities wrapping a `Join` activity on an event
defp do_handle_incoming_accept_join(join_object, %Actor{} = actor_accepting) do
with {:join_event,
{:ok,
%Participant{role: :not_approved, actor: actor, id: join_id, event: event} =
participant}} <-
with {:join_event, {:ok, %Participant{role: :not_approved, event: event} = participant}} <-
{:join_event, get_participant(join_object)},
# TODO: The actor that accepts the Join activity may another one that the event organizer ?
# Or maybe for groups it's the group that sends the Accept activity
{:same_actor, true} <- {:same_actor, actor_accepting.id == event.organizer_actor_id},
{:ok, activity, _} <-
{:ok, %Activity{} = activity, %Participant{role: :participant} = participant} <-
ActivityPub.accept(
%{
to: [actor.url],
actor: actor_accepting.url,
object: join_object,
local: false
},
"#{MobilizonWeb.Endpoint.url()}/accept/join/#{join_id}"
:join,
participant,
%{role: :participant},
false
),
{:ok, %Participant{role: :participant}} <-
Events.update_participant(participant, %{"role" => :participant}),
:ok <-
Participation.send_emails_to_local_user(participant) do
{:ok, activity, participant}
@@ -684,7 +677,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
def prepare_object(object) do
object
# |> set_sensitive
|> add_hashtags
# |> add_hashtags
|> add_mention_tags
# |> add_emoji_tags
|> add_attributed_to
@@ -781,6 +774,9 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
def add_mention_tags(object) do
Logger.debug("add mention tags")
Logger.debug(inspect(object))
recipients =
(object["to"] ++ (object["cc"] || [])) -- ["https://www.w3.org/ns/activitystreams#Public"]
@@ -795,7 +791,11 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end)
|> Enum.filter(& &1)
|> Enum.map(fn actor ->
%{"type" => "Mention", "href" => actor.url, "name" => "@#{actor.preferred_username}"}
%{
"type" => "Mention",
"href" => actor.url,
"name" => "@#{Actor.preferred_username_and_domain(actor)}"
}
end)
tags = object["tag"] || []
@@ -854,6 +854,7 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
@spec fetch_obj_helper(map() | String.t()) :: Event.t() | Comment.t() | Actor.t() | any()
def fetch_obj_helper(object) do
Logger.debug("fetch_obj_helper")
Logger.debug("Fetching object #{inspect(object)}")
case object |> Utils.get_url() |> ActivityPub.fetch_object_from_url() do
@@ -867,6 +868,8 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
end
def fetch_obj_helper_as_activity_streams(object) do
Logger.debug("fetch_obj_helper_as_activity_streams")
with {:ok, object} <- fetch_obj_helper(object) do
{:ok, Convertible.model_to_as(object)}
end

View File

@@ -238,8 +238,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
@doc """
Save picture data from %Plug.Upload{} and return AS Link data.
"""
def make_picture_data(%Plug.Upload{} = picture) do
case MobilizonWeb.Upload.store(picture) do
def make_picture_data(%Plug.Upload{} = picture, opts) do
case MobilizonWeb.Upload.store(picture, opts) do
{:ok, picture} ->
picture
@@ -636,18 +636,39 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
Make create activity data
"""
@spec make_create_data(map(), map()) :: map()
def make_create_data(params, additional \\ %{}) do
def make_create_data(object, additional \\ %{}) do
Logger.debug("Making create data")
Logger.debug(inspect(params))
published = params.published || make_date()
Logger.debug(inspect(object))
Logger.debug(inspect(additional))
%{
"type" => "Create",
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.url,
"object" => params.object,
"published" => published,
"id" => params.object["id"] <> "/activity"
"to" => object["to"],
"cc" => object["cc"],
"actor" => object["actor"],
"object" => object,
"published" => make_date(),
"id" => object["id"] <> "/activity"
}
|> Map.merge(additional)
end
@doc """
Make update activity data
"""
@spec make_update_data(map(), map()) :: map()
def make_update_data(object, additional \\ %{}) do
Logger.debug("Making update data")
Logger.debug(inspect(object))
Logger.debug(inspect(additional))
%{
"type" => "Update",
"to" => object["to"],
"cc" => object["cc"],
"actor" => object["actor"],
"object" => object,
"id" => object["id"] <> "/activity"
}
|> Map.merge(additional)
end
@@ -688,6 +709,22 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
}
end
@doc """
Make accept join activity data
"""
@spec make_accept_join_data(map(), map()) :: map()
def make_accept_join_data(object, additional \\ %{}) do
%{
"type" => "Accept",
"to" => object["to"],
"cc" => object["cc"],
"actor" => object["actor"],
"object" => object,
"id" => object["id"] <> "/activity"
}
|> Map.merge(additional)
end
@doc """
Converts PEM encoded keys to a public key representation
"""

View File

@@ -53,7 +53,7 @@ defmodule Mobilizon.Service.Federator do
Logger.debug(inspect(params))
case Transmogrifier.handle_incoming(params) do
{:ok, activity, _} ->
{:ok, activity, _data} ->
{:ok, activity}
%Activity{} ->

View File

@@ -52,7 +52,7 @@ defmodule Mobilizon.Service.HTTPSignatures.Signature do
@spec get_public_key_for_url(String.t()) ::
{:ok, String.t()} | {:error, :actor_fetch_error | :pem_decode_error}
def get_public_key_for_url(url) do
with {:ok, %Actor{keys: keys}} <- ActivityPub.get_or_fetch_by_url(url),
with {:ok, %Actor{keys: keys}} <- ActivityPub.get_or_fetch_actor_by_url(url),
{:ok, public_key} <- prepare_public_key(keys) do
{:ok, public_key}
else