Put HTTPSignatures, WebFinger and ActivityStream alongside with ActivityPub
This commit is contained in:
11
lib/federation/activity_stream/converter.ex
Normal file
11
lib/federation/activity_stream/converter.ex
Normal file
@@ -0,0 +1,11 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.Converter do
|
||||
@moduledoc """
|
||||
Converter behaviour.
|
||||
|
||||
This module allows to convert from ActivityStream format to our own internal
|
||||
one, and back.
|
||||
"""
|
||||
|
||||
@callback as_to_model_data(map) :: map
|
||||
@callback model_to_as(struct) :: map
|
||||
end
|
||||
116
lib/federation/activity_stream/converter/actor.ex
Normal file
116
lib/federation/activity_stream/converter/actor.ex
Normal file
@@ -0,0 +1,116 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.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.Federation.ActivityPub.Utils
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: ActorModel do
|
||||
alias Mobilizon.Federation.ActivityStream.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(data) do
|
||||
avatar =
|
||||
data["icon"]["url"] &&
|
||||
%{
|
||||
"name" => data["icon"]["name"] || "avatar",
|
||||
"url" => MobilizonWeb.MediaProxy.url(data["icon"]["url"])
|
||||
}
|
||||
|
||||
banner =
|
||||
data["image"]["url"] &&
|
||||
%{
|
||||
"name" => data["image"]["name"] || "banner",
|
||||
"url" => MobilizonWeb.MediaProxy.url(data["image"]["url"])
|
||||
}
|
||||
|
||||
actor_data = %{
|
||||
url: data["id"],
|
||||
avatar: avatar,
|
||||
banner: banner,
|
||||
name: data["name"],
|
||||
preferred_username: data["preferredUsername"],
|
||||
summary: data["summary"],
|
||||
keys: data["publicKey"]["publicKeyPem"],
|
||||
inbox_url: data["inbox"],
|
||||
outbox_url: data["outbox"],
|
||||
following_url: data["following"],
|
||||
followers_url: data["followers"],
|
||||
shared_inbox_url: data["endpoints"]["sharedInbox"],
|
||||
domain: URI.parse(data["id"]).host,
|
||||
manually_approves_followers: data["manuallyApprovesFollowers"],
|
||||
type: data["type"]
|
||||
}
|
||||
|
||||
{:ok, actor_data}
|
||||
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
|
||||
actor_data = %{
|
||||
"id" => actor.url,
|
||||
"type" => actor.type,
|
||||
"preferredUsername" => actor.preferred_username,
|
||||
"name" => actor.name,
|
||||
"summary" => actor.summary,
|
||||
"following" => actor.following_url,
|
||||
"followers" => actor.followers_url,
|
||||
"inbox" => actor.inbox_url,
|
||||
"outbox" => actor.outbox_url,
|
||||
"url" => actor.url,
|
||||
"endpoints" => %{
|
||||
"sharedInbox" => actor.shared_inbox_url
|
||||
},
|
||||
"manuallyApprovesFollowers" => actor.manually_approves_followers,
|
||||
"publicKey" => %{
|
||||
"id" => "#{actor.url}#main-key",
|
||||
"owner" => actor.url,
|
||||
"publicKeyPem" =>
|
||||
if(is_nil(actor.domain) and not is_nil(actor.keys),
|
||||
do: Utils.pem_to_public_key_pem(actor.keys),
|
||||
else: actor.keys
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
actor_data =
|
||||
if is_nil(actor.avatar) do
|
||||
actor_data
|
||||
else
|
||||
Map.put(actor_data, "icon", %{
|
||||
"type" => "Image",
|
||||
"mediaType" => actor.avatar.content_type,
|
||||
"url" => actor.avatar.url
|
||||
})
|
||||
end
|
||||
|
||||
if is_nil(actor.banner) do
|
||||
actor_data
|
||||
else
|
||||
Map.put(actor_data, "image", %{
|
||||
"type" => "Image",
|
||||
"mediaType" => actor.banner.content_type,
|
||||
"url" => actor.banner.url
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
79
lib/federation/activity_stream/converter/address.ex
Normal file
79
lib/federation/activity_stream/converter/address.ex
Normal file
@@ -0,0 +1,79 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.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.Federation.ActivityStream.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["latitude"]) or is_nil(object["longitude"]) do
|
||||
res
|
||||
else
|
||||
geo = %Geo.Point{
|
||||
coordinates: {object["latitude"], object["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
|
||||
res
|
||||
|> Map.put("latitude", address.geom.coordinates |> elem(0))
|
||||
|> Map.put("longitude", address.geom.coordinates |> elem(1))
|
||||
end
|
||||
end
|
||||
end
|
||||
150
lib/federation/activity_stream/converter/comment.ex
Normal file
150
lib/federation/activity_stream/converter/comment.ex
Normal file
@@ -0,0 +1,150 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.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.Tombstone, as: TombstoneModel
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityPub.Visibility
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: CommentModel do
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Comment, as: CommentConverter
|
||||
|
||||
defdelegate model_to_as(comment), to: CommentConverter
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an AP object data to our internal data structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: {:ok, map} | {:error, any()}
|
||||
def as_to_model_data(object) do
|
||||
Logger.debug("We're converting raw ActivityStream data to a comment entity")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
||||
{:ok, %Actor{id: actor_id, domain: domain}} <-
|
||||
ActivityPub.get_or_fetch_actor_by_url(author_url),
|
||||
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(Map.get(object, "tag", []))},
|
||||
{:mentions, mentions} <-
|
||||
{:mentions, ConverterUtils.fetch_mentions(Map.get(object, "tag", []))} do
|
||||
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"],
|
||||
tags: tags,
|
||||
mentions: mentions,
|
||||
local: is_nil(domain),
|
||||
visibility: if(Visibility.is_public?(object), do: :public, else: :private)
|
||||
}
|
||||
|
||||
# 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())
|
||||
|> Map.put(:event_id, comment.event_id)
|
||||
|
||||
# Anything else is kind of a MP
|
||||
{:error, parent} ->
|
||||
Logger.warn("Parent object is something we don't handle")
|
||||
Logger.debug(inspect(parent))
|
||||
data
|
||||
end
|
||||
else
|
||||
Logger.debug("No parent object for this comment")
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
{:ok, data}
|
||||
else
|
||||
err ->
|
||||
{:error, err}
|
||||
end
|
||||
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{deleted_at: nil} = comment) do
|
||||
to =
|
||||
if comment.visibility == :public,
|
||||
do: ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
else: [comment.actor.followers_url]
|
||||
|
||||
object = %{
|
||||
"type" => "Note",
|
||||
"to" => to,
|
||||
"cc" => [],
|
||||
"content" => comment.text,
|
||||
"mediaType" => "text/html",
|
||||
"actor" => comment.actor.url,
|
||||
"attributedTo" => comment.actor.url,
|
||||
"uuid" => comment.uuid,
|
||||
"id" => comment.url,
|
||||
"tag" =>
|
||||
ConverterUtils.build_mentions(comment.mentions) ++ ConverterUtils.build_tags(comment.tags)
|
||||
}
|
||||
|
||||
cond do
|
||||
comment.in_reply_to_comment ->
|
||||
Map.put(object, "inReplyTo", comment.in_reply_to_comment.url)
|
||||
|
||||
comment.event ->
|
||||
Map.put(object, "inReplyTo", comment.event.url)
|
||||
|
||||
true ->
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
@impl Converter
|
||||
@spec model_to_as(CommentModel.t()) :: map
|
||||
@doc """
|
||||
A "soft-deleted" comment is a tombstone
|
||||
"""
|
||||
def model_to_as(%CommentModel{} = comment) do
|
||||
Convertible.model_to_as(%TombstoneModel{
|
||||
uri: comment.url,
|
||||
inserted_at: comment.deleted_at
|
||||
})
|
||||
end
|
||||
end
|
||||
199
lib/federation/activity_stream/converter/event.ex
Normal file
199
lib/federation/activity_stream/converter/event.ex
Normal file
@@ -0,0 +1,199 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
@moduledoc """
|
||||
Event converter.
|
||||
|
||||
This module allows to convert events from ActivityStream format to our own
|
||||
internal one, and back.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Addresses
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Events.Event, as: EventModel
|
||||
alias Mobilizon.Media.Picture
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Address, as: AddressConverter
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Picture, as: PictureConverter
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: EventModel do
|
||||
alias Mobilizon.Federation.ActivityStream.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) :: {:ok, map()} | {:error, any()}
|
||||
def as_to_model_data(object) do
|
||||
Logger.debug("event as_to_model_data")
|
||||
Logger.debug(inspect(object))
|
||||
|
||||
with author_url <- Map.get(object, "actor") || Map.get(object, "attributedTo"),
|
||||
{:actor, {:ok, %Actor{id: actor_id, domain: actor_domain}}} <-
|
||||
{:actor, ActivityPub.get_or_fetch_actor_by_url(author_url)},
|
||||
{:address, address_id} <-
|
||||
{:address, get_address(object["location"])},
|
||||
{:tags, tags} <- {:tags, ConverterUtils.fetch_tags(object["tag"])},
|
||||
{:mentions, mentions} <- {:mentions, ConverterUtils.fetch_mentions(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,
|
||||
{:ok, %Picture{id: picture_id}} <-
|
||||
object["attachment"]
|
||||
|> hd
|
||||
|> PictureConverter.find_or_create_picture(actor_id) do
|
||||
picture_id
|
||||
else
|
||||
_err ->
|
||||
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: Map.get(object, "joinMode", "free"),
|
||||
local: is_nil(actor_domain),
|
||||
options: options,
|
||||
status: object |> Map.get("ical:status", "CONFIRMED") |> String.downcase(),
|
||||
online_address: object["onlineAddress"],
|
||||
phone_address: object["phoneAddress"],
|
||||
draft: false,
|
||||
url: object["id"],
|
||||
uuid: object["uuid"],
|
||||
tags: tags,
|
||||
mentions: mentions,
|
||||
physical_address_id: address_id,
|
||||
updated_at: object["updated"],
|
||||
publish_at: object["published"]
|
||||
}
|
||||
|
||||
{:ok, entity}
|
||||
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,
|
||||
"published" => (event.publish_at || event.inserted_at) |> date_to_string(),
|
||||
"updated" => event.updated_at |> date_to_string(),
|
||||
"mediaType" => "text/html",
|
||||
"startTime" => event.begins_on |> date_to_string(),
|
||||
"joinMode" => to_string(event.join_options),
|
||||
"endTime" => event.ends_on |> date_to_string(),
|
||||
"tag" => event.tags |> ConverterUtils.build_tags(),
|
||||
"maximumAttendeeCapacity" => event.options.maximum_attendee_capacity,
|
||||
"repliesModerationOption" => event.options.comment_moderation,
|
||||
"commentsEnabled" => event.options.comment_moderation == :allow_all,
|
||||
# "draft" => event.draft,
|
||||
"ical:status" => event.status |> to_string |> String.upcase(),
|
||||
"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
|
||||
%{
|
||||
maximum_attendee_capacity: object["maximumAttendeeCapacity"],
|
||||
comment_moderation:
|
||||
Map.get(
|
||||
object,
|
||||
"repliesModerationOption",
|
||||
if(Map.get(object, "commentsEnabled", true), do: :allow_all, else: :closed)
|
||||
)
|
||||
}
|
||||
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 = AddressConverter.as_to_model_data(map)
|
||||
|
||||
case Addresses.create_address(map) do
|
||||
{:ok, %Address{id: address_id}} ->
|
||||
address_id
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@ap_public "https://www.w3.org/ns/activitystreams#Public"
|
||||
|
||||
defp get_visibility(object), do: if(@ap_public in object["to"], do: :public, else: :unlisted)
|
||||
|
||||
@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
|
||||
105
lib/federation/activity_stream/converter/flag.ex
Normal file
105
lib/federation/activity_stream/converter/flag.ex
Normal file
@@ -0,0 +1,105 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.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.Federation.ActivityPub.Relay
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: Report do
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Flag, as: FlagConverter
|
||||
|
||||
defdelegate model_to_as(report), to: FlagConverter
|
||||
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
|
||||
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(Report.t()) :: map
|
||||
def model_to_as(%Report{} = report) do
|
||||
object = [report.reported.url] ++ Enum.map(report.comments, fn comment -> comment.url end)
|
||||
|
||||
object = if report.event, do: object ++ [report.event.url], else: object
|
||||
|
||||
%{
|
||||
"type" => "Flag",
|
||||
"actor" => Relay.get_actor().url,
|
||||
"id" => report.url,
|
||||
"content" => report.content,
|
||||
"object" => object
|
||||
}
|
||||
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 actor and the event 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
|
||||
37
lib/federation/activity_stream/converter/follower.ex
Normal file
37
lib/federation/activity_stream/converter/follower.ex
Normal file
@@ -0,0 +1,37 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.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.Actor
|
||||
alias Mobilizon.Actors.Follower, as: FollowerModel
|
||||
|
||||
alias Mobilizon.Federation.ActivityStream.Convertible
|
||||
|
||||
defimpl Convertible, for: FollowerModel do
|
||||
alias Mobilizon.Federation.ActivityStream.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
|
||||
31
lib/federation/activity_stream/converter/participant.ex
Normal file
31
lib/federation/activity_stream/converter/participant.ex
Normal file
@@ -0,0 +1,31 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.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.Federation.ActivityStream.Convertible
|
||||
|
||||
defimpl Convertible, for: ParticipantModel do
|
||||
alias Mobilizon.Federation.ActivityStream.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
|
||||
56
lib/federation/activity_stream/converter/picture.ex
Normal file
56
lib/federation/activity_stream/converter/picture.ex
Normal file
@@ -0,0 +1,56 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.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",
|
||||
"mediaType" => file.content_type,
|
||||
"url" => file.url,
|
||||
"name" => file.name
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Save picture data from raw data and return AS Link data.
|
||||
"""
|
||||
def find_or_create_picture(%{"type" => "Link", "href" => url}, actor_id),
|
||||
do: find_or_create_picture(url, actor_id)
|
||||
|
||||
def find_or_create_picture(
|
||||
%{"type" => "Document", "url" => picture_url, "name" => name},
|
||||
actor_id
|
||||
)
|
||||
when is_bitstring(picture_url) do
|
||||
with {:ok, %HTTPoison.Response{body: body}} <- HTTPoison.get(picture_url),
|
||||
{:ok, %{name: name, url: url, content_type: content_type, size: size}} <-
|
||||
MobilizonWeb.Upload.store(%{body: body, name: name}),
|
||||
{:picture_exists, nil} <- {:picture_exists, Mobilizon.Media.get_picture_by_url(url)} do
|
||||
Mobilizon.Media.create_picture(%{
|
||||
"file" => %{
|
||||
"url" => url,
|
||||
"name" => name,
|
||||
"content_type" => content_type,
|
||||
"size" => size
|
||||
},
|
||||
"actor_id" => actor_id
|
||||
})
|
||||
else
|
||||
{:picture_exists, %PictureModel{file: _file} = picture} ->
|
||||
{:ok, picture}
|
||||
|
||||
err ->
|
||||
err
|
||||
end
|
||||
end
|
||||
end
|
||||
41
lib/federation/activity_stream/converter/tombstone.ex
Normal file
41
lib/federation/activity_stream/converter/tombstone.ex
Normal file
@@ -0,0 +1,41 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.Converter.Tombstone do
|
||||
@moduledoc """
|
||||
Comment converter.
|
||||
|
||||
This module allows to convert Tombstone models to ActivityStreams data
|
||||
"""
|
||||
|
||||
alias Mobilizon.Tombstone, as: TombstoneModel
|
||||
|
||||
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
|
||||
|
||||
require Logger
|
||||
|
||||
@behaviour Converter
|
||||
|
||||
defimpl Convertible, for: TombstoneModel do
|
||||
alias Mobilizon.Federation.ActivityStream.Converter.Tombstone, as: TombstoneConverter
|
||||
|
||||
defdelegate model_to_as(comment), to: TombstoneConverter
|
||||
end
|
||||
|
||||
@doc """
|
||||
Make an AS tombstone object from an existing `Tombstone` structure.
|
||||
"""
|
||||
@impl Converter
|
||||
@spec model_to_as(TombstoneModel.t()) :: map
|
||||
def model_to_as(%TombstoneModel{} = tombstone) do
|
||||
%{
|
||||
"type" => "Tombstone",
|
||||
"id" => tombstone.uri,
|
||||
"deleted" => tombstone.inserted_at
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converting an Tombstone to an object makes no sense, nevertheless…
|
||||
"""
|
||||
@impl Converter
|
||||
@spec as_to_model_data(map) :: map
|
||||
def as_to_model_data(object), do: object
|
||||
end
|
||||
112
lib/federation/activity_stream/converter/utils.ex
Normal file
112
lib/federation/activity_stream/converter/utils.ex
Normal file
@@ -0,0 +1,112 @@
|
||||
defmodule Mobilizon.Federation.ActivityStream.Converter.Utils do
|
||||
@moduledoc """
|
||||
Various utils for converters.
|
||||
"""
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.Tag
|
||||
alias Mobilizon.Mention
|
||||
alias Mobilizon.Storage.Repo
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
|
||||
require Logger
|
||||
|
||||
@spec fetch_tags([String.t()]) :: [Tag.t()]
|
||||
def fetch_tags(tags) when is_list(tags) do
|
||||
Logger.debug("fetching tags")
|
||||
Logger.debug(inspect(tags))
|
||||
|
||||
tags |> Enum.flat_map(&fetch_tag/1) |> Enum.uniq() |> Enum.map(&existing_tag_or_data/1)
|
||||
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
|
||||
|
||||
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(%{title: title}), do: [title]
|
||||
|
||||
defp fetch_tag(tag) when is_map(tag) do
|
||||
case tag["type"] do
|
||||
"Hashtag" ->
|
||||
[tag_without_hash(tag["name"])]
|
||||
|
||||
_err ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_tag(tag) when is_bitstring(tag), do: [tag_without_hash(tag)]
|
||||
|
||||
defp tag_without_hash("#" <> tag_title), do: tag_title
|
||||
defp tag_without_hash(tag_title), do: tag_title
|
||||
|
||||
defp existing_tag_or_data(tag_title) do
|
||||
case Events.get_tag_by_title(tag_title) do
|
||||
%Tag{} = tag -> %{title: tag.title, id: tag.id}
|
||||
nil -> %{title: tag_title}
|
||||
end
|
||||
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
|
||||
10
lib/federation/activity_stream/convertible.ex
Normal file
10
lib/federation/activity_stream/convertible.ex
Normal file
@@ -0,0 +1,10 @@
|
||||
defprotocol Mobilizon.Federation.ActivityStream.Convertible do
|
||||
@moduledoc """
|
||||
Convertible protocol.
|
||||
"""
|
||||
|
||||
@type activity_streams :: map
|
||||
|
||||
@spec model_to_as(t) :: activity_streams
|
||||
def model_to_as(convertible)
|
||||
end
|
||||
Reference in New Issue
Block a user