Put HTTPSignatures, WebFinger and ActivityStream alongside with ActivityPub

This commit is contained in:
rustra
2020-01-22 22:40:40 +01:00
parent cdb520a95b
commit d1251280c5
28 changed files with 57 additions and 64 deletions

View File

@@ -22,13 +22,13 @@ defmodule Mobilizon.Federation.ActivityPub do
Federator,
Relay,
Transmogrifier,
Visibility,
WebFinger
Visibility
}
alias Mobilizon.Federation.ActivityPub.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityPub.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.Converter.Utils, as: ConverterUtils
alias Mobilizon.Federation.HTTPSignatures.Signature
alias Mobilizon.Federation.WebFinger
alias MobilizonWeb.API.Utils, as: APIUtils
alias MobilizonWeb.Email.{Admin, Mailer}
@@ -38,7 +38,7 @@ defmodule Mobilizon.Federation.ActivityPub do
@doc """
Wraps an object into an activity
"""
@spec create_activity(map(), boolean()) :: {:ok, %Activity{}}
@spec create_activity(map, boolean) :: {:ok, Activity.t()}
def create_activity(map, local \\ true) when is_map(map) do
with map <- lazy_put_activity_defaults(map) do
{:ok,

View File

@@ -1,11 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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

View File

@@ -1,116 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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.ActivityPub.ActivityStream.{Converter, Convertible}
@behaviour Converter
defimpl Convertible, for: ActorModel do
alias Mobilizon.Federation.ActivityPub.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

View File

@@ -1,79 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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.ActivityPub.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

View File

@@ -1,150 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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.ActivityPub.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityPub.ActivityStream.Converter.Utils, as: ConverterUtils
require Logger
@behaviour Converter
defimpl Convertible, for: CommentModel do
alias Mobilizon.Federation.ActivityPub.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

View File

@@ -1,199 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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.ActivityPub.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityPub.ActivityStream.Converter.Address, as: AddressConverter
alias Mobilizon.Federation.ActivityPub.ActivityStream.Converter.Picture, as: PictureConverter
alias Mobilizon.Federation.ActivityPub.ActivityStream.Converter.Utils, as: ConverterUtils
require Logger
@behaviour Converter
defimpl Convertible, for: EventModel do
alias Mobilizon.Federation.ActivityPub.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

View File

@@ -1,105 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityPub.Relay
@behaviour Converter
defimpl Convertible, for: Report do
alias Mobilizon.Federation.ActivityPub.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

View File

@@ -1,37 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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.ActivityPub.ActivityStream.Convertible
defimpl Convertible, for: FollowerModel do
alias Mobilizon.Federation.ActivityPub.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

View File

@@ -1,31 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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.ActivityPub.ActivityStream.Convertible
defimpl Convertible, for: ParticipantModel do
alias Mobilizon.Federation.ActivityPub.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

View File

@@ -1,62 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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

View File

@@ -1,41 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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.ActivityPub.ActivityStream.{Converter, Convertible}
require Logger
@behaviour Converter
defimpl Convertible, for: TombstoneModel do
alias Mobilizon.Federation.ActivityPub.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

View File

@@ -1,114 +0,0 @@
defmodule Mobilizon.Federation.ActivityPub.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
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

View File

@@ -1,10 +0,0 @@
defprotocol Mobilizon.Federation.ActivityPub.ActivityStream.Convertible do
@moduledoc """
Convertible protocol.
"""
@type activity_streams :: map
@spec model_to_as(t) :: activity_streams
def model_to_as(convertible)
end

View File

@@ -1,117 +0,0 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/signature.ex
defmodule Mobilizon.Federation.HTTPSignatures.Signature do
@moduledoc """
Adapter for the `HTTPSignatures` lib that handles signing and providing public keys to verify HTTPSignatures
"""
@behaviour HTTPSignatures.Adapter
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub
require Logger
@spec key_id_to_actor_url(String.t()) :: String.t()
def key_id_to_actor_url(key_id) do
%{path: path} =
uri =
key_id
|> URI.parse()
|> Map.put(:fragment, nil)
uri =
if is_nil(path) do
uri
else
Map.put(uri, :path, String.trim_trailing(path, "/publickey"))
end
URI.to_string(uri)
end
@doc """
Convert internal PEM encoded keys to public key format.
"""
@spec prepare_public_key(String.t()) :: {:ok, tuple} | {:error, :pem_decode_error}
def prepare_public_key(public_key_code) do
case :public_key.pem_decode(public_key_code) do
[public_key_entry] ->
{:ok, :public_key.pem_entry_decode(public_key_entry)}
_ ->
{:error, :pem_decode_error}
end
end
# Gets a public key for a given ActivityPub actor ID (url).
@spec get_public_key_for_url(String.t()) ::
{:ok, String.t()} | {:error, :actor_fetch_error | :pem_decode_error}
defp get_public_key_for_url(url) do
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
{:error, :pem_decode_error} ->
Logger.error("Error while decoding PEM")
{:error, :pem_decode_error}
_ ->
Logger.error("Unable to fetch actor, so no keys for you")
{:error, :actor_fetch_error}
end
end
def fetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid),
:ok <- Logger.debug("Fetching public key for #{actor_id}"),
{:ok, public_key} <- get_public_key_for_url(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def refetch_public_key(conn) do
with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn),
actor_id <- key_id_to_actor_url(kid),
:ok <- Logger.debug("Refetching public key for #{actor_id}"),
{:ok, _actor} <- ActivityPub.make_actor_from_url(actor_id),
{:ok, public_key} <- get_public_key_for_url(actor_id) do
{:ok, public_key}
else
e ->
{:error, e}
end
end
def sign(%Actor{keys: keys} = actor, headers) do
Logger.debug("Signing on behalf of #{actor.url}")
Logger.debug("headers")
Logger.debug(inspect(headers))
with {:ok, key} <- prepare_public_key(keys) do
HTTPSignatures.sign(key, actor.url <> "#main-key", headers)
end
end
def generate_date_header, do: generate_date_header(NaiveDateTime.utc_now())
def generate_date_header(%NaiveDateTime{} = date) do
Timex.format!(date, "{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
end
def generate_request_target(method, path), do: "#{method} #{path}"
def build_digest(body) do
"SHA-256=#{:sha256 |> :crypto.hash(body) |> Base.encode64()}"
end
end

View File

@@ -12,7 +12,8 @@ defmodule Mobilizon.Federation.ActivityPub.Relay do
alias Mobilizon.Actors.{Actor, Follower}
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier, WebFinger}
alias Mobilizon.Federation.ActivityPub.{Activity, Transmogrifier}
alias Mobilizon.Federation.WebFinger
alias MobilizonWeb.API.Follows

View File

@@ -15,7 +15,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
alias Mobilizon.Federation.ActivityPub
alias Mobilizon.Federation.ActivityPub.{Activity, Utils}
alias Mobilizon.Federation.ActivityPub.ActivityStream.{Converter, Convertible}
alias Mobilizon.Federation.ActivityStream.{Converter, Convertible}
alias MobilizonWeb.Email.Participation

View File

@@ -13,7 +13,7 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
alias Mobilizon.Media.Picture
alias Mobilizon.Federation.ActivityPub.{Activity, Federator, Relay}
alias Mobilizon.Federation.ActivityPub.ActivityStream.Converter
alias Mobilizon.Federation.ActivityStream.Converter
alias Mobilizon.Federation.HTTPSignatures
require Logger

View File

@@ -1,127 +0,0 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/web_finger/web_finger.ex
defmodule Mobilizon.Federation.ActivityPub.WebFinger do
@moduledoc """
Performs the WebFinger requests and responses (JSON only).
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Federation.ActivityPub.WebFinger.XmlBuilder
require Jason
require Logger
def host_meta do
base_url = MobilizonWeb.Endpoint.url()
{
:XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
{
:Link,
%{
rel: "lrdd",
type: "application/xrd+xml",
template: "#{base_url}/.well-known/webfinger?resource={uri}"
}
}
}
|> XmlBuilder.to_doc()
end
def webfinger(resource, "JSON") do
host = MobilizonWeb.Endpoint.host()
regex = ~r/(acct:)?(?<name>\w+)@#{host}/
with %{"name" => name} <- Regex.named_captures(regex, resource),
%Actor{} = actor <- Actors.get_local_actor_by_name(name) do
{:ok, represent_actor(actor, "JSON")}
else
_e ->
case Actors.get_actor_by_url(resource) do
{:ok, %Actor{} = actor} when not is_nil(actor) ->
{:ok, represent_actor(actor, "JSON")}
_e ->
{:error, "Couldn't find actor"}
end
end
end
@spec represent_actor(Actor.t()) :: struct()
def represent_actor(actor), do: represent_actor(actor, "JSON")
@spec represent_actor(Actor.t(), String.t()) :: struct()
def represent_actor(actor, "JSON") do
%{
"subject" => "acct:#{actor.preferred_username}@#{MobilizonWeb.Endpoint.host()}",
"aliases" => [actor.url],
"links" => [
%{"rel" => "self", "type" => "application/activity+json", "href" => actor.url},
%{
"rel" => "https://webfinger.net/rel/profile-page/",
"type" => "text/html",
"href" => actor.url
}
]
}
end
defp webfinger_from_json(doc) do
data =
Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
case {link["type"], link["rel"]} do
{"application/activity+json", "self"} ->
Map.put(data, "url", link["href"])
_ ->
Logger.debug(fn ->
"Unhandled type: #{inspect(link["type"])}"
end)
data
end
end)
{:ok, data}
end
def finger(actor) do
actor = String.trim_leading(actor, "@")
domain =
case String.split(actor, "@") do
[_name, domain] ->
domain
_e ->
URI.parse(actor).host
end
address = "http://#{domain}/.well-known/webfinger?resource=acct:#{actor}"
Logger.debug(inspect(address))
with false <- is_nil(domain),
{:ok, %HTTPoison.Response{} = response} <-
HTTPoison.get(
address,
[Accept: "application/json, application/activity+json, application/jrd+json"],
follow_redirect: true
),
%{status_code: status_code, body: body} when status_code in 200..299 <- response,
{:ok, doc} <- Jason.decode(body) do
webfinger_from_json(doc)
else
e ->
Logger.debug(fn -> "Couldn't finger #{actor}" end)
Logger.debug(fn -> inspect(e) end)
{:error, e}
end
end
end

View File

@@ -1,46 +0,0 @@
# Portions of this file are derived from Pleroma:
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social>
# SPDX-License-Identifier: AGPL-3.0-only
# Upstream: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/xml_builder.ex
defmodule Mobilizon.Federation.ActivityPub.WebFinger.XmlBuilder do
@moduledoc """
Builds XRD for WebFinger host_meta.
"""
def to_xml({tag, attributes, content}) do
open_tag = make_open_tag(tag, attributes)
content_xml = to_xml(content)
"<#{open_tag}>#{content_xml}</#{tag}>"
end
def to_xml({tag, %{} = attributes}) do
open_tag = make_open_tag(tag, attributes)
"<#{open_tag} />"
end
def to_xml({tag, content}), do: to_xml({tag, %{}, content})
def to_xml(content) when is_binary(content), do: to_string(content)
def to_xml(content) when is_list(content) do
content
|> Enum.map(&to_xml/1)
|> Enum.join()
end
def to_xml(%NaiveDateTime{} = time), do: NaiveDateTime.to_iso8601(time)
def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(content)
defp make_open_tag(tag, attributes) do
attributes_string =
attributes
|> Enum.map(fn {attribute, value} -> "#{attribute}=\"#{value}\"" end)
|> Enum.join(" ")
[tag, attributes_string] |> Enum.join(" ") |> String.trim()
end
end