Fix software design suggestions

This commit is contained in:
miffigriffi
2019-09-22 18:29:13 +02:00
committed by Thomas Citharel
parent aed824f1aa
commit 0a0d07cf38
36 changed files with 292 additions and 208 deletions

View File

@@ -0,0 +1,73 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Actor do
@moduledoc """
Actor converter.
This module allows to convert events from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.Actors.Actor, as: ActorModel
alias Mobilizon.Service.ActivityPub.{Converter, Convertible}
@behaviour Converter
defimpl Convertible, for: ActorModel do
alias Mobilizon.Service.ActivityPub.Converter.Actor, as: ActorConverter
defdelegate model_to_as(actor), to: ActorConverter
end
@doc """
Converts an AP object data to our internal data structure.
"""
@impl Converter
@spec as_to_model_data(map) :: map
def as_to_model_data(object) do
avatar =
object["icon"]["url"] &&
%{
"name" => object["icon"]["name"] || "avatar",
"url" => object["icon"]["url"]
}
banner =
object["image"]["url"] &&
%{
"name" => object["image"]["name"] || "banner",
"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"]
}
end
@doc """
Convert an actor struct to an ActivityStream representation.
"""
@impl Converter
@spec model_to_as(ActorModel.t()) :: map
def model_to_as(%ActorModel{} = actor) do
%{
"type" => Atom.to_string(actor.type),
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"preferred_username" => actor.preferred_username,
"name" => actor.name,
"summary" => actor.summary,
"following" => ActorModel.build_url(actor.preferred_username, :following),
"followers" => ActorModel.build_url(actor.preferred_username, :followers),
"inbox" => ActorModel.build_url(actor.preferred_username, :inbox),
"outbox" => ActorModel.build_url(actor.preferred_username, :outbox),
"id" => ActorModel.build_url(actor.preferred_username, :page),
"url" => actor.url
}
end
end

View File

@@ -0,0 +1,80 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Address do
@moduledoc """
Address converter.
This module allows to convert reports from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.Addresses.Address, as: AddressModel
alias Mobilizon.Service.ActivityPub.Converter
@behaviour Converter
@doc """
Converts an AP object data to our internal data structure.
"""
@impl Converter
@spec as_to_model_data(map) :: map
def as_to_model_data(object) do
res = %{
"description" => object["name"],
"url" => object["url"]
}
res =
if is_nil(object["address"]) do
res
else
Map.merge(res, %{
"country" => object["address"]["addressCountry"],
"postal_code" => object["address"]["postalCode"],
"region" => object["address"]["addressRegion"],
"street" => object["address"]["streetAddress"],
"locality" => object["address"]["addressLocality"]
})
end
if is_nil(object["geo"]) do
res
else
geo = %Geo.Point{
coordinates: {object["geo"]["latitude"], object["geo"]["longitude"]},
srid: 4326
}
Map.put(res, "geom", geo)
end
end
@doc """
Convert an event struct to an ActivityStream representation.
"""
@impl Converter
@spec model_to_as(AddressModel.t()) :: map
def model_to_as(%AddressModel{} = address) do
res = %{
"type" => "Place",
"name" => address.description,
"id" => address.url,
"address" => %{
"type" => "PostalAddress",
"streetAddress" => address.street,
"postalCode" => address.postal_code,
"addressLocality" => address.locality,
"addressRegion" => address.region,
"addressCountry" => address.country
}
}
if is_nil(address.geom) do
res
else
Map.put(res, "geo", %{
"type" => "GeoCoordinates",
"latitude" => address.geom.coordinates |> elem(0),
"longitude" => address.geom.coordinates |> elem(1)
})
end
end
end

View File

@@ -0,0 +1,104 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Comment do
@moduledoc """
Comment converter.
This module allows to convert events from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment, as: CommentModel
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub
alias Mobilizon.Service.ActivityPub.{Converter, Convertible}
require Logger
@behaviour Converter
defimpl Convertible, for: CommentModel do
alias Mobilizon.Service.ActivityPub.Converter.Comment, as: CommentConverter
defdelegate model_to_as(comment), to: CommentConverter
end
@doc """
Converts an AP object data to our internal data structure.
"""
@impl Converter
@spec as_to_model_data(map) :: map
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(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"]
}
# We fetch the parent object
Logger.debug("We're fetching the parent object")
data =
if Map.has_key?(object, "inReplyTo") && object["inReplyTo"] != nil &&
object["inReplyTo"] != "" do
Logger.debug(fn -> "Object has inReplyTo #{object["inReplyTo"]}" end)
case ActivityPub.fetch_object_from_url(object["inReplyTo"]) do
# Reply to an event (Event)
{:ok, %Event{id: id}} ->
Logger.debug("Parent object is an event")
data |> Map.put("event_id", id)
# Reply to a comment (Comment)
{:ok, %CommentModel{id: id} = comment} ->
Logger.debug("Parent object is another comment")
data
|> Map.put("in_reply_to_comment_id", id)
|> Map.put("origin_comment_id", comment |> CommentModel.get_thread_id())
# 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
data
end
@doc """
Make an AS comment object from an existing `Comment` structure.
"""
@impl Converter
@spec model_to_as(CommentModel.t()) :: map
def model_to_as(%CommentModel{} = comment) do
object = %{
"type" => "Note",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"content" => comment.text,
"actor" => comment.actor.url,
"attributedTo" => comment.actor.url,
"uuid" => comment.uuid,
"id" => comment.url
}
if comment.in_reply_to_comment do
object |> Map.put("inReplyTo", comment.in_reply_to_comment.url || comment.event.url)
else
object
end
end
end

View File

@@ -0,0 +1,262 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Event do
@moduledoc """
Event converter.
This module allows to convert events from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.{Actors, Addresses, Events, Media}
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
alias Mobilizon.Events.Event, as: EventModel
alias Mobilizon.Events.{EventOptions, Tag}
alias Mobilizon.Media.Picture
alias Mobilizon.Service.ActivityPub.{Converter, Convertible, Utils}
alias Mobilizon.Service.ActivityPub.Converter.Address, as: AddressConverter
alias Mobilizon.Service.ActivityPub.Converter.Picture, as: PictureConverter
require Logger
@behaviour Converter
defimpl Convertible, for: EventModel do
alias Mobilizon.Service.ActivityPub.Converter.Event, as: EventConverter
defdelegate model_to_as(event), to: EventConverter
end
@doc """
Converts an AP object data to our internal data structure.
"""
@impl Converter
@spec as_to_model_data(map) :: map
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"])},
{:address, address_id} <-
{:address, get_address(object["location"])},
{:tags, tags} <- {:tags, fetch_tags(object["tag"])},
{:visibility, visibility} <- {:visibility, get_visibility(object)},
{:options, options} <- {:options, get_options(object)} do
picture_id =
with true <- Map.has_key?(object, "attachment") && length(object["attachment"]) > 0,
%Picture{id: picture_id} <-
Media.get_picture_by_url(
object["attachment"]
|> hd
|> Map.get("url")
|> hd
|> Map.get("href")
) do
picture_id
else
_ -> nil
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"],
"url" => object["id"],
"uuid" => object["uuid"],
"tags" => tags,
"physical_address_id" => address_id
}
{:ok, Map.put(entity, "options", options)}
else
error ->
{:error, error}
end
end
@doc """
Convert an event struct to an ActivityStream representation.
"""
@impl Converter
@spec model_to_as(EventModel.t()) :: map
def model_to_as(%EventModel{} = event) do
to =
if event.visibility == :public,
do: ["https://www.w3.org/ns/activitystreams#Public"],
else: [event.organizer_actor.followers_url]
res = %{
"type" => "Event",
"to" => to,
"cc" => [],
"attributedTo" => event.organizer_actor.url,
"name" => event.title,
"actor" => event.organizer_actor.url,
"uuid" => event.uuid,
"category" => event.category,
"content" => event.description,
"publish_at" => (event.publish_at || event.inserted_at) |> date_to_string(),
"updated_at" => event.updated_at |> date_to_string(),
"mediaType" => "text/html",
"startTime" => event.begins_on |> date_to_string(),
"endTime" => event.ends_on |> date_to_string(),
"tag" => event.tags |> build_tags(),
"id" => event.url,
"url" => event.url
}
res =
if is_nil(event.physical_address),
do: res,
else: Map.put(res, "location", AddressConverter.model_to_as(event.physical_address))
if is_nil(event.picture),
do: res,
else: Map.put(res, "attachment", [PictureConverter.model_to_as(event.picture)])
end
# Get only elements that we have in EventOptions
@spec get_options(map) :: map
defp get_options(object) do
keys =
EventOptions
|> struct
|> Map.keys()
|> List.delete(:__struct__)
|> Enum.map(&Utils.camelize/1)
Enum.reduce(object, %{}, fn {key, value}, acc ->
(value && key in keys && Map.put(acc, Utils.underscore(key), value)) ||
acc
end)
end
@spec get_address(map | binary | nil) :: integer | nil
defp get_address(address_url) when is_bitstring(address_url) do
get_address(%{"id" => address_url})
end
defp get_address(%{"id" => url} = map) when is_map(map) and is_binary(url) do
Logger.debug("Address with an URL, let's check against our own database")
case Addresses.get_address_by_url(url) do
%Address{id: address_id} ->
address_id
_ ->
Logger.debug("not in our database, let's try to create it")
map = Map.put(map, "url", map["id"])
do_get_address(map)
end
end
defp get_address(map) when is_map(map) do
do_get_address(map)
end
defp get_address(nil), do: nil
@spec do_get_address(map) :: integer | nil
defp do_get_address(map) do
map = Mobilizon.Service.ActivityPub.Converter.Address.as_to_model_data(map)
case Addresses.create_address(map) do
{:ok, %Address{id: address_id}} ->
address_id
_ ->
nil
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
cond do
@ap_public in object["to"] -> :public
@ap_public in object["cc"] -> :unlisted
true -> :private
end
end
@doc """
Convert an event struct to an ActivityStream representation
"""
@impl Converter
@spec model_to_as(EventModel.t()) :: map()
def model_to_as(%EventModel{} = event) do
to =
if event.visibility == :public,
do: ["https://www.w3.org/ns/activitystreams#Public"],
else: [event.organizer_actor.followers_url]
res = %{
"type" => "Event",
"to" => to,
"cc" => [],
"attributedTo" => event.organizer_actor.url,
"name" => event.title,
"actor" => event.organizer_actor.url,
"uuid" => event.uuid,
"category" => event.category,
"content" => event.description,
"publish_at" => (event.publish_at || event.inserted_at) |> date_to_string(),
"updated_at" => event.updated_at |> date_to_string(),
"mediaType" => "text/html",
"startTime" => event.begins_on |> date_to_string(),
"endTime" => event.ends_on |> date_to_string(),
"joinOptions" => to_string(event.join_options),
"tag" => event.tags |> build_tags(),
"id" => event.url,
"url" => event.url
}
res =
if is_nil(event.physical_address),
do: res,
else: Map.put(res, "location", AddressConverter.model_to_as(event.physical_address))
if is_nil(event.picture),
do: res,
else: Map.put(res, "attachment", [Utils.make_picture_data(event.picture)])
end
@spec date_to_string(DateTime.t() | nil) :: String.t()
defp date_to_string(nil), do: nil
defp date_to_string(%DateTime{} = date), do: DateTime.to_iso8601(date)
end

View File

@@ -0,0 +1,92 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Flag do
@moduledoc """
Flag converter.
This module allows to convert reports from ActivityStream format to our own
internal one, and back.
Note: Reports are named Flag in AS.
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Reports.Report
alias Mobilizon.Service.ActivityPub.Converter
@behaviour Converter
@doc """
Converts an AP object data to our internal data structure.
"""
@impl Converter
@spec as_to_model_data(map) :: map
def as_to_model_data(object) do
with params <- as_to_model(object) do
%{
"reporter_id" => params["reporter"].id,
"uri" => params["uri"],
"content" => params["content"],
"reported_id" => params["reported"].id,
"event_id" => (!is_nil(params["event"]) && params["event"].id) || nil,
"comments" => params["comments"]
}
end
end
@doc """
Convert an event struct to an ActivityStream representation
"""
@impl Converter
@spec model_to_as(EventModel.t()) :: map
def model_to_as(%Report{} = report) do
%{
"type" => "Flag",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"actor" => report.reporter.url,
"id" => report.url
}
end
@spec as_to_model(map) :: map
def as_to_model(%{"object" => objects} = object) do
with {:ok, %Actor{} = reporter} <- Actors.get_actor_by_url(object["actor"]),
%Actor{} = reported <-
Enum.reduce_while(objects, nil, fn url, _ ->
case Actors.get_actor_by_url(url) do
{:ok, %Actor{} = actor} ->
{:halt, actor}
_ ->
{:cont, nil}
end
end),
event <-
Enum.reduce_while(objects, nil, fn url, _ ->
case Events.get_event_by_url(url) do
%Event{} = event ->
{:halt, event}
_ ->
{:cont, nil}
end
end),
# Remove the reported user from the object list.
comments <-
Enum.filter(objects, fn url ->
!(url == reported.url || (!is_nil(event) && event.url == url))
end),
comments <- Enum.map(comments, &Events.get_comment_from_url/1) do
%{
"reporter" => reporter,
"uri" => object["id"],
"content" => object["content"],
"reported" => reported,
"event" => event,
"comments" => comments
}
end
end
end

View File

@@ -0,0 +1,31 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Participant do
@moduledoc """
Participant converter.
This module allows to convert reports from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.Events.Participant, as: ParticipantModel
alias Mobilizon.Service.ActivityPub.Convertible
defimpl Convertible, for: ParticipantModel do
alias Mobilizon.Service.ActivityPub.Converter.Participant, as: ParticipantConverter
defdelegate model_to_as(participant), to: ParticipantConverter
end
@doc """
Convert an event struct to an ActivityStream representation.
"""
@spec model_to_as(ParticipantModel.t()) :: map
def model_to_as(%ParticipantModel{} = participant) do
%{
"type" => "Join",
"id" => participant.url,
"actor" => participant.actor.url,
"object" => participant.event.url
}
end
end

View File

@@ -0,0 +1,28 @@
defmodule Mobilizon.Service.ActivityPub.Converter.Picture do
@moduledoc """
Picture converter.
This module allows to convert events from ActivityStream format to our own
internal one, and back.
"""
alias Mobilizon.Media.Picture, as: PictureModel
@doc """
Convert a picture struct to an ActivityStream representation.
"""
@spec model_to_as(PictureModel.t()) :: map
def model_to_as(%PictureModel{file: file}) do
%{
"type" => "Document",
"url" => [
%{
"type" => "Link",
"mediaType" => file.content_type,
"href" => file.url
}
],
"name" => file.name
}
end
end