808 lines
24 KiB
Elixir
808 lines
24 KiB
Elixir
# 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/activity_pub/utils.ex
|
||
|
||
defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||
@moduledoc """
|
||
Various ActivityPub related utils.
|
||
"""
|
||
|
||
alias Mobilizon.Actors
|
||
alias Mobilizon.Actors.Actor
|
||
alias Mobilizon.Medias.Media
|
||
|
||
alias Mobilizon.Federation.ActivityPub.{Actions, Activity, Federator}
|
||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||
alias Mobilizon.Federation.ActivityStream.Converter
|
||
alias Mobilizon.Federation.HTTPSignatures
|
||
alias Mobilizon.Web.Endpoint
|
||
|
||
require Logger
|
||
|
||
@actor_types ["Group", "Person", "Application"]
|
||
@all_actor_types @actor_types ++ ["Organization", "Service"]
|
||
@ap_public_audience "https://www.w3.org/ns/activitystreams#Public"
|
||
|
||
# Wraps an object into an activity
|
||
@spec create_activity(map(), boolean()) :: {:ok, Activity.t()}
|
||
def create_activity(map, local) when is_map(map) do
|
||
with map <- lazy_put_activity_defaults(map) do
|
||
{:ok,
|
||
%Activity{
|
||
data: map,
|
||
local: local,
|
||
actor: map["actor"],
|
||
recipients: get_recipients(map)
|
||
}}
|
||
end
|
||
end
|
||
|
||
# Get recipients for an activity or object
|
||
@spec get_recipients(map()) :: list()
|
||
defp get_recipients(data) do
|
||
Map.get(data, "to", []) ++ Map.get(data, "cc", [])
|
||
end
|
||
|
||
# Some implementations send the actor URI as the actor field, others send the entire actor object,
|
||
# so figure out what the actor's URI is based on what we have.
|
||
@spec get_url(map() | String.t() | list(String.t()) | any()) :: String.t() | nil
|
||
def get_url(%{"id" => id}), do: id
|
||
def get_url(id) when is_binary(id), do: id
|
||
def get_url(ids) when is_list(ids), do: get_url(hd(ids))
|
||
def get_url(_), do: nil
|
||
|
||
@spec make_json_ld_header :: map()
|
||
def make_json_ld_header do
|
||
%{
|
||
"@context" => [
|
||
"https://www.w3.org/ns/activitystreams",
|
||
"https://w3id.org/security/v1",
|
||
%{
|
||
"@language" => "und",
|
||
"sc" => "http://schema.org#",
|
||
"ical" => "http://www.w3.org/2002/12/cal/ical#",
|
||
"pt" => "https://joinpeertube.org/ns#",
|
||
"toot" => "http://joinmastodon.org/ns#",
|
||
"discoverable" => "toot:discoverable",
|
||
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
|
||
"Hashtag" => "as:Hashtag",
|
||
"category" => "sc:category",
|
||
"uuid" => "sc:identifier",
|
||
"maximumAttendeeCapacity" => "sc:maximumAttendeeCapacity",
|
||
"remainingAttendeeCapacity" => "sc:remainingAttendeeCapacity",
|
||
"location" => %{
|
||
"@id" => "sc:location",
|
||
"@type" => "sc:Place"
|
||
},
|
||
"PostalAddress" => "sc:PostalAddress",
|
||
"address" => %{
|
||
"@id" => "sc:address",
|
||
"@type" => "sc:PostalAddress"
|
||
},
|
||
"addressCountry" => "sc:addressCountry",
|
||
"addressRegion" => "sc:addressRegion",
|
||
"postalCode" => "sc:postalCode",
|
||
"addressLocality" => "sc:addressLocality",
|
||
"streetAddress" => "sc:streetAddress",
|
||
"mz" => "https://joinmobilizon.org/ns#",
|
||
"repliesModerationOptionType" => %{
|
||
"@id" => "mz:repliesModerationOptionType",
|
||
"@type" => "rdfs:Class"
|
||
},
|
||
"repliesModerationOption" => %{
|
||
"@id" => "mz:repliesModerationOption",
|
||
"@type" => "mz:repliesModerationOptionType"
|
||
},
|
||
"commentsEnabled" => %{
|
||
"@type" => "sc:Boolean",
|
||
"@id" => "pt:commentsEnabled"
|
||
},
|
||
"joinModeType" => %{
|
||
"@id" => "mz:joinModeType",
|
||
"@type" => "rdfs:Class"
|
||
},
|
||
"joinMode" => %{
|
||
"@id" => "mz:joinMode",
|
||
"@type" => "mz:joinModeType"
|
||
},
|
||
"anonymousParticipationEnabled" => %{
|
||
"@id" => "mz:anonymousParticipationEnabled",
|
||
"@type" => "sc:Boolean"
|
||
},
|
||
"participationMessage" => %{
|
||
"@id" => "mz:participationMessage",
|
||
"@type" => "sc:Text"
|
||
},
|
||
"participantCount" => %{
|
||
"@id" => "mz:participantCount",
|
||
"@type" => "sc:Integer"
|
||
},
|
||
"memberCount" => %{
|
||
"@id" => "mz:memberCount",
|
||
"@type" => "sc:Integer"
|
||
},
|
||
"isOnline" => %{
|
||
"@type" => "sc:Boolean",
|
||
"@id" => "mz:isOnline"
|
||
},
|
||
"externalParticipationUrl" => %{
|
||
"@id" => "mz:externalParticipationUrl",
|
||
"@type" => "sc:URL"
|
||
},
|
||
"PropertyValue" => "sc:PropertyValue",
|
||
"value" => "sc:value",
|
||
"propertyID" => "sc:propertyID",
|
||
"inLanguage" => "sc:inLanguage",
|
||
"timezone" => %{
|
||
"@id" => "mz:timezone",
|
||
"@type" => "sc:Text"
|
||
},
|
||
"discussions" => %{
|
||
"@id" => "mz:discussions",
|
||
"@type" => "@id"
|
||
},
|
||
"events" => %{
|
||
"@id" => "mz:events",
|
||
"@type" => "@id"
|
||
},
|
||
"members" => %{
|
||
"@id" => "mz:members",
|
||
"@type" => "@id"
|
||
},
|
||
"openness" => %{
|
||
"@id" => "mz:openness",
|
||
"@type" => "@id"
|
||
},
|
||
"posts" => %{
|
||
"@id" => "mz:posts",
|
||
"@type" => "@id"
|
||
},
|
||
"resources" => %{
|
||
"@id" => "mz:resources",
|
||
"@type" => "@id"
|
||
},
|
||
"todos" => %{
|
||
"@id" => "mz:todos",
|
||
"@type" => "@id"
|
||
},
|
||
"status" => %{
|
||
"@id" => "ical:status",
|
||
"@type" => "ical:status"
|
||
}
|
||
}
|
||
]
|
||
}
|
||
end
|
||
|
||
@spec make_date :: String.t()
|
||
def make_date do
|
||
DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
|
||
end
|
||
|
||
@doc """
|
||
Enqueues an activity for federation if it's local
|
||
"""
|
||
@spec maybe_federate(activity :: Activity.t()) :: :ok
|
||
def maybe_federate(%Activity{local: true} = activity) do
|
||
Logger.debug("Maybe federate an activity")
|
||
|
||
if Mobilizon.Config.get!([:instance, :federating]) do
|
||
priority =
|
||
case activity.data["type"] do
|
||
"Delete" -> 10
|
||
"Create" -> 1
|
||
_ -> 5
|
||
end
|
||
|
||
Federator.enqueue(:publish, activity, priority)
|
||
end
|
||
|
||
:ok
|
||
end
|
||
|
||
def maybe_federate(_), do: :ok
|
||
|
||
@doc """
|
||
Applies to activities sent by group members from outside this instance to a group of this instance,
|
||
we then need to relay (`Announce`) the object to other members on other instances.
|
||
"""
|
||
@spec maybe_relay_if_group_activity(Activity.t(), Actor.t() | nil | list(Actor.t())) :: :ok
|
||
def maybe_relay_if_group_activity(activity, attributed_to \\ nil)
|
||
|
||
def maybe_relay_if_group_activity(
|
||
%Activity{data: %{"object" => object}},
|
||
_attributed_to
|
||
)
|
||
when is_map(object) do
|
||
Logger.debug("Maybe relay if group activity (object is map)")
|
||
Logger.debug(inspect(object))
|
||
do_maybe_relay_if_group_activity(object, object["attributedTo"])
|
||
end
|
||
|
||
# When doing a delete the object is just an AP ID string, so we pass the attributed_to actor as well
|
||
def maybe_relay_if_group_activity(
|
||
%Activity{data: %{"object" => object}},
|
||
%Actor{url: attributed_to_url}
|
||
)
|
||
when is_binary(object) and is_binary(attributed_to_url) do
|
||
Logger.debug("Maybe relay if group activity (object is binary)")
|
||
do_maybe_relay_if_group_activity(object, attributed_to_url)
|
||
end
|
||
|
||
def maybe_relay_if_group_activity(_activity, _attributedTo) do
|
||
Logger.debug("Will not replay : not a group activity")
|
||
:ok
|
||
end
|
||
|
||
# TODO: Is this a map or a String?
|
||
@spec do_maybe_relay_if_group_activity(map() | String.t(), list(String.t()) | String.t()) :: :ok
|
||
defp do_maybe_relay_if_group_activity(object, attributed_to) when is_list(attributed_to),
|
||
do: do_maybe_relay_if_group_activity(object, hd(attributed_to))
|
||
|
||
defp do_maybe_relay_if_group_activity(object, attributed_to) when is_binary(attributed_to) do
|
||
Logger.debug("Let's try to relay group activity")
|
||
id = "#{Endpoint.url()}/announces/#{Ecto.UUID.generate()}"
|
||
|
||
case Actors.get_local_group_by_url(attributed_to) do
|
||
%Actor{} = group ->
|
||
case Actions.Announce.announce(group, object, id, true, false) do
|
||
{:ok, _activity, _object} ->
|
||
Logger.info("Forwarded activity to external members of the group")
|
||
:ok
|
||
|
||
{:error, err} ->
|
||
Logger.info("Failed to forward activity to external members of the group")
|
||
Logger.debug(inspect(err))
|
||
:error
|
||
end
|
||
|
||
nil ->
|
||
:ok
|
||
end
|
||
end
|
||
|
||
defp do_maybe_relay_if_group_activity(_, attributed_to) do
|
||
Logger.debug("Will not relay group activity, attributed to is : #{inspect(attributed_to)}")
|
||
end
|
||
|
||
@spec remote_actors(list(String.t())) :: list(Actor.t())
|
||
def remote_actors(recipients) do
|
||
recipients
|
||
|> Enum.map(fn url -> ActivityPubActor.get_or_fetch_actor_by_url(url) end)
|
||
|> Enum.map(fn {status, actor} ->
|
||
case status do
|
||
:ok ->
|
||
actor
|
||
|
||
_ ->
|
||
nil
|
||
end
|
||
end)
|
||
|> Enum.map(& &1)
|
||
|> Enum.filter(fn actor -> actor && !is_nil(actor.domain) end)
|
||
end
|
||
|
||
@doc """
|
||
Adds an id and a published data if they aren't there,
|
||
also adds it to an included object
|
||
"""
|
||
@spec lazy_put_activity_defaults(map()) :: map()
|
||
def lazy_put_activity_defaults(%{"object" => _object} = map) do
|
||
if is_map(map["object"]) do
|
||
object = lazy_put_object_defaults(map["object"])
|
||
%{map | "object" => object}
|
||
else
|
||
map
|
||
end
|
||
end
|
||
|
||
@doc """
|
||
Adds an id and published date if they aren't there.
|
||
"""
|
||
def lazy_put_object_defaults(map) when is_map(map) do
|
||
Map.put_new_lazy(map, "published", &make_date/0)
|
||
end
|
||
|
||
@spec get_actor(map()) :: String.t() | nil
|
||
def get_actor(%{"actor" => actor}) when is_binary(actor) do
|
||
actor
|
||
end
|
||
|
||
def get_actor(%{"actor" => [actor | tail] = actor_list} = object)
|
||
when is_list(actor_list) and length(actor_list) > 0 do
|
||
res =
|
||
try do
|
||
object
|
||
|> Map.put("actor", actor)
|
||
|> get_actor()
|
||
rescue
|
||
ArgumentError -> nil
|
||
end
|
||
|
||
case res do
|
||
id when is_binary(id) ->
|
||
id
|
||
|
||
_ ->
|
||
object
|
||
|> Map.put("actor", tail)
|
||
|> get_actor()
|
||
end
|
||
end
|
||
|
||
def get_actor(%{"actor" => %{"id" => id, "type" => type}})
|
||
when is_binary(id) and type in @all_actor_types do
|
||
id
|
||
end
|
||
|
||
def get_actor(%{"actor" => _, "attributedTo" => actor}) when not is_nil(actor) do
|
||
get_actor(%{"actor" => actor})
|
||
end
|
||
|
||
def get_actor(%{"actor" => %{"id" => id, "type" => type}})
|
||
when is_binary(id) do
|
||
raise ArgumentError,
|
||
message: "Object contains an actor object with invalid type: #{inspect(type)}"
|
||
end
|
||
|
||
def get_actor(%{"actor" => nil, "attributedTo" => nil} = object) do
|
||
raise ArgumentError,
|
||
message: "Object contains both actor and attributedTo fields being null: #{inspect(object)}"
|
||
end
|
||
|
||
def get_actor(%{"actor" => _}) do
|
||
raise ArgumentError, message: "Object contains not actor information"
|
||
end
|
||
|
||
@doc """
|
||
Checks that an incoming AP object's actor matches the domain it came from.
|
||
|
||
Takes the actor or attributedTo attributes (considers only the first elem if they're an array)
|
||
"""
|
||
@spec origin_check?(String.t(), map()) :: boolean()
|
||
def origin_check?(id, %{"type" => "Tombstone", "id" => tombstone_id}), do: id == tombstone_id
|
||
|
||
def origin_check?(id, %{"actor" => actor, "attributedTo" => _attributed_to} = params)
|
||
when not is_nil(actor) and actor != "" do
|
||
params = Map.delete(params, "attributedTo")
|
||
origin_check?(id, params)
|
||
end
|
||
|
||
def origin_check?(id, %{"attributedTo" => actor} = params) do
|
||
params = params |> Map.put("actor", actor) |> Map.delete("attributedTo")
|
||
origin_check?(id, params)
|
||
end
|
||
|
||
def origin_check?(id, %{"actor" => actor} = params)
|
||
when not is_nil(actor) and is_list(actor) and length(actor) > 0 do
|
||
origin_check?(id, Map.put(params, "actor", hd(actor)))
|
||
end
|
||
|
||
def origin_check?(id, %{"actor" => actor} = params)
|
||
when not is_nil(actor) do
|
||
actor = get_actor(params)
|
||
Logger.debug("Performing origin check on #{id} and #{actor} URIs")
|
||
are_same_origin?(id, actor)
|
||
end
|
||
|
||
def origin_check?(id, %{"type" => type, "id" => actor_id} = _params)
|
||
when type in ["Actor", "Person", "Group", "Application"],
|
||
do: id == actor_id
|
||
|
||
def origin_check?(_id, %{"actor" => nil} = _args), do: false
|
||
|
||
def origin_check?(_id, _args), do: false
|
||
|
||
@spec are_same_origin?(String.t(), String.t()) :: boolean()
|
||
def are_same_origin?(url_1, url_2) when is_binary(url_1) and is_binary(url_2) do
|
||
uri_1 = URI.parse(url_1)
|
||
uri_2 = URI.parse(url_2)
|
||
|
||
compare_uris?(uri_1, uri_2)
|
||
end
|
||
|
||
@spec compare_uris?(URI.t(), URI.t()) :: boolean()
|
||
defp compare_uris?(%URI{} = id_uri, %URI{} = other_uri),
|
||
do: id_uri.host == other_uri.host && id_uri.port == other_uri.port
|
||
|
||
@spec origin_check_from_id?(String.t(), String.t()) :: boolean()
|
||
def origin_check_from_id?(id, other_id) when is_binary(other_id) do
|
||
id_uri = URI.parse(id)
|
||
other_uri = URI.parse(other_id)
|
||
|
||
compare_uris?(id_uri, other_uri)
|
||
end
|
||
|
||
@spec origin_check_from_id?(String.t(), map()) :: boolean()
|
||
def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id),
|
||
do: origin_check_from_id?(id, other_id)
|
||
|
||
@doc """
|
||
Return AS Link data from
|
||
|
||
* a `Plug.Upload` struct, stored an returned
|
||
* a `Media`, directly returned
|
||
* a map containing media information, stored, saved and returned
|
||
|
||
Save media data from %Plug.Upload{} and return AS Link data.
|
||
"""
|
||
def make_media_data(%Plug.Upload{} = media, opts) do
|
||
case Mobilizon.Web.Upload.store(media, opts) do
|
||
{:ok, media} ->
|
||
media
|
||
|
||
{:error, _err} ->
|
||
nil
|
||
end
|
||
end
|
||
|
||
def make_media_data(media) when is_map(media) do
|
||
with {:ok, %{url: url} = uploaded} <-
|
||
Mobilizon.Web.Upload.store(media.file),
|
||
{:media_exists, nil} <- {:media_exists, Mobilizon.Medias.get_media_by_url(url)},
|
||
{:ok, %Media{file: _file} = media} <-
|
||
Mobilizon.Medias.create_media(%{
|
||
file: Map.take(uploaded, [:url, :name, :content_type, :size]),
|
||
metadata: Map.take(uploaded, [:width, :height, :blurhash]),
|
||
actor_id: media.actor_id
|
||
}) do
|
||
Converter.Media.model_to_as(media)
|
||
else
|
||
{:media_exists, %Media{file: _file} = media} ->
|
||
Converter.Media.model_to_as(media)
|
||
|
||
err ->
|
||
err
|
||
end
|
||
end
|
||
|
||
def make_media_data(nil), do: nil
|
||
|
||
@doc """
|
||
Make announce activity data for the given actor and object
|
||
"""
|
||
def make_announce_data(actor, object, activity_id, public \\ true)
|
||
|
||
def make_announce_data(
|
||
%Actor{} = actor,
|
||
%{"id" => url, "type" => type} = _object,
|
||
activity_id,
|
||
public
|
||
)
|
||
when type in @actor_types do
|
||
do_make_announce_data(actor, url, url, activity_id, public)
|
||
end
|
||
|
||
def make_announce_data(
|
||
%Actor{} = actor,
|
||
%{"actor" => object_actor_url} = object,
|
||
activity_id,
|
||
public
|
||
) do
|
||
do_make_announce_data(
|
||
actor,
|
||
object_actor_url,
|
||
object,
|
||
activity_id,
|
||
public
|
||
)
|
||
end
|
||
|
||
defp do_make_announce_data(
|
||
%Actor{type: actor_type} = actor,
|
||
object_actor_url,
|
||
object,
|
||
activity_id,
|
||
public
|
||
) do
|
||
{to, cc} =
|
||
if public do
|
||
Logger.debug("Making announce data for a public object")
|
||
|
||
{[actor.followers_url, object_actor_url], [@ap_public_audience]}
|
||
else
|
||
Logger.debug("Making announce data for a private object")
|
||
|
||
if actor_type == :Group do
|
||
Logger.debug("Making announce data for a group private object")
|
||
|
||
to =
|
||
Map.get(object, "to", []) ++
|
||
Map.get(object, "cc", []) ++ [actor.followers_url, actor.members_url]
|
||
|
||
{to, []}
|
||
else
|
||
Logger.debug("Making announce data for a private object")
|
||
{[actor.followers_url], []}
|
||
end
|
||
end
|
||
|
||
data = %{
|
||
"type" => "Announce",
|
||
"actor" => actor.url,
|
||
"object" => object,
|
||
"to" => to,
|
||
"cc" => cc
|
||
}
|
||
|
||
data =
|
||
if object["attributedTo"],
|
||
do: Map.put(data, "attributedTo", object["attributedTo"]),
|
||
else: data
|
||
|
||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||
end
|
||
|
||
@doc """
|
||
Make unannounce activity data for the given actor and object
|
||
"""
|
||
def make_unannounce_data(
|
||
%Actor{url: url} = actor,
|
||
activity,
|
||
activity_id
|
||
) do
|
||
data = %{
|
||
"type" => "Undo",
|
||
"actor" => url,
|
||
"object" => activity,
|
||
"to" => [actor.followers_url, actor.url],
|
||
"cc" => [@ap_public_audience]
|
||
}
|
||
|
||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||
end
|
||
|
||
#### Unfollow-related helpers
|
||
|
||
@spec make_unfollow_data(Actor.t(), Actor.t(), map(), String.t()) :: map()
|
||
def make_unfollow_data(
|
||
%Actor{url: follower_url},
|
||
%Actor{url: followed_url},
|
||
follow_activity,
|
||
activity_id
|
||
) do
|
||
data = %{
|
||
"type" => "Undo",
|
||
"actor" => follower_url,
|
||
"to" => [followed_url],
|
||
"object" => follow_activity.data
|
||
}
|
||
|
||
if activity_id, do: Map.put(data, "id", activity_id), else: data
|
||
end
|
||
|
||
#### Create-related helpers
|
||
|
||
@doc """
|
||
Make create activity data
|
||
"""
|
||
@spec make_create_data(map(), map()) :: map()
|
||
def make_create_data(object, additional \\ %{}) do
|
||
Logger.debug("Making create data")
|
||
Logger.debug(inspect(object))
|
||
Logger.debug(inspect(additional))
|
||
|
||
%{
|
||
"type" => "Create",
|
||
"to" => object["to"],
|
||
"cc" => object["cc"],
|
||
"attributedTo" => object["attributedTo"] || object["actor"],
|
||
"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"],
|
||
"attributedTo" => object["attributedTo"] || object["actor"],
|
||
"actor" => object["actor"],
|
||
"object" => object,
|
||
"id" => object["id"] <> "/activity"
|
||
}
|
||
|> Map.merge(additional)
|
||
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"],
|
||
"object" => object,
|
||
"id" => object["id"] <> "/activity"
|
||
}
|
||
|> Map.merge(additional)
|
||
end
|
||
|
||
@doc """
|
||
Make add activity data
|
||
"""
|
||
@spec make_add_data(map(), map(), map()) :: map()
|
||
def make_add_data(object, target, additional \\ %{}) do
|
||
Logger.debug("Making add data")
|
||
Logger.debug(inspect(object))
|
||
Logger.debug(inspect(additional))
|
||
|
||
%{
|
||
"type" => "Add",
|
||
"to" => object["to"],
|
||
"cc" => object["cc"],
|
||
"actor" => object["actor"],
|
||
"object" => object,
|
||
"target" => Map.get(target, :url, target),
|
||
"id" => object["id"] <> "/add"
|
||
}
|
||
|> Map.merge(additional)
|
||
end
|
||
|
||
@doc """
|
||
Make move activity data
|
||
"""
|
||
@spec make_move_data(map(), map(), map(), map()) :: map()
|
||
def make_move_data(object, origin, target, additional \\ %{}) do
|
||
Logger.debug("Making move data")
|
||
Logger.debug(inspect(object))
|
||
Logger.debug(inspect(origin))
|
||
Logger.debug(inspect(target))
|
||
Logger.debug(inspect(additional))
|
||
|
||
%{
|
||
"type" => "Move",
|
||
"to" => object["to"],
|
||
"cc" => object["cc"],
|
||
"actor" => object["actor"],
|
||
"object" => object,
|
||
"origin" => if(is_nil(origin), do: origin, else: Map.get(origin, :url, origin)),
|
||
"target" => if(is_nil(target), do: target, else: Map.get(target, :url, target)),
|
||
"id" => object["id"] <> "/move"
|
||
}
|
||
|> Map.merge(additional)
|
||
end
|
||
|
||
@doc """
|
||
Converts PEM encoded keys to a public key representation
|
||
"""
|
||
@spec pem_to_public_key_pem(String.t()) :: String.t() | {:error, :no_publickey_found}
|
||
def pem_to_public_key_pem(pem) do
|
||
case :public_key.pem_decode(pem) do
|
||
[key_code] ->
|
||
public_key = pem_to_public_key(key_code)
|
||
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
|
||
:public_key.pem_encode([public_key])
|
||
|
||
_ ->
|
||
{:error, :no_publickey_found}
|
||
end
|
||
end
|
||
|
||
@spec pem_to_public_key(String.t()) :: {:RSAPublicKey, any(), any()}
|
||
defp pem_to_public_key(key_code) do
|
||
case :public_key.pem_entry_decode(key_code) do
|
||
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} ->
|
||
{:RSAPublicKey, modulus, exponent}
|
||
|
||
{:RSAPublicKey, modulus, exponent} ->
|
||
{:RSAPublicKey, modulus, exponent}
|
||
end
|
||
end
|
||
|
||
@spec make_signature(Actor.t(), String.t(), DateTime.t()) :: list({atom(), String.t()})
|
||
defp make_signature(actor, id, date) do
|
||
uri = URI.parse(id)
|
||
|
||
signature =
|
||
actor
|
||
|> HTTPSignatures.Signature.sign(%{
|
||
"(request-target)": "get #{uri.path}",
|
||
host: uri.host,
|
||
date: date
|
||
})
|
||
|
||
[{:Signature, signature}]
|
||
end
|
||
|
||
@doc """
|
||
Sign a request with an actor.
|
||
"""
|
||
@spec sign_fetch(Enum.t(), Actor.t(), String.t(), String.t(), Keyword.t()) :: Enum.t()
|
||
def sign_fetch(headers, actor, id, date, options \\ []) do
|
||
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) and
|
||
Keyword.get(options, :ignore_sign_object_fetches, false) == false do
|
||
headers ++ make_signature(actor, id, date)
|
||
else
|
||
headers
|
||
end
|
||
end
|
||
|
||
@doc """
|
||
Add the Date header to the request if we sign object fetches
|
||
"""
|
||
@spec maybe_date_fetch(Enum.t(), String.t()) :: Enum.t()
|
||
def maybe_date_fetch(headers, date) do
|
||
if Mobilizon.Config.get([:activitypub, :sign_object_fetches]) do
|
||
headers ++ [{:Date, date}]
|
||
else
|
||
headers
|
||
end
|
||
end
|
||
|
||
def check_for_actor_key_rotation(%Actor{} = actor) do
|
||
if Actors.should_rotate_actor_key(actor) do
|
||
Actors.schedule_key_rotation(
|
||
actor,
|
||
Application.get_env(:mobilizon, :activitypub)[:actor_key_rotation_delay]
|
||
)
|
||
end
|
||
|
||
:ok
|
||
end
|
||
|
||
@spec label_in_collection?(any(), any()) :: boolean()
|
||
defp label_in_collection?(url, coll) when is_binary(coll), do: url == coll
|
||
defp label_in_collection?(url, coll) when is_list(coll), do: url in coll
|
||
defp label_in_collection?(_, _), do: false
|
||
|
||
@spec label_in_message?(String.t(), map()) :: boolean()
|
||
def label_in_message?(label, params),
|
||
do:
|
||
[params["to"], params["cc"], params["bto"], params["bcc"]]
|
||
|> Enum.any?(&label_in_collection?(label, &1))
|
||
|
||
@spec unaddressed_message?(map()) :: boolean()
|
||
def unaddressed_message?(params),
|
||
do:
|
||
[params["to"], params["cc"], params["bto"], params["bcc"]]
|
||
|> Enum.all?(&is_nil(&1))
|
||
|
||
@spec recipient_in_message(Actor.t(), Actor.t(), map()) :: boolean()
|
||
def recipient_in_message(%Actor{url: url} = _recipient, %Actor{} = _actor, params),
|
||
do: label_in_message?(url, params) || unaddressed_message?(params)
|
||
|
||
defp extract_list(target) when is_binary(target), do: [target]
|
||
defp extract_list(lst) when is_list(lst), do: lst
|
||
defp extract_list(_), do: []
|
||
|
||
def maybe_splice_recipient(url, params) do
|
||
need_splice? =
|
||
!label_in_collection?(url, params["to"]) &&
|
||
!label_in_collection?(url, params["cc"])
|
||
|
||
if need_splice? do
|
||
cc_list = extract_list(params["cc"])
|
||
Map.put(params, "cc", [url | cc_list])
|
||
else
|
||
params
|
||
end
|
||
end
|
||
|
||
@schemes_with_no_port ["http", "https"]
|
||
|
||
def create_full_domain_string(%URI{host: host, port: nil}), do: host
|
||
|
||
def create_full_domain_string(%URI{host: host, port: port}) do
|
||
if port in Enum.map(@schemes_with_no_port, &URI.default_port/1) do
|
||
host
|
||
else
|
||
"#{host}:#{port}"
|
||
end
|
||
end
|
||
end
|