Refactor media upload

Use Upload Media logic from Pleroma

Backend changes for picture upload

Move AS <-> Model conversion to separate module

Front changes

Downgrade apollo-client: https://github.com/Akryum/vue-apollo/issues/577

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2019-05-22 14:12:11 +02:00
parent 9724bc8e9f
commit f90089e1bf
113 changed files with 4718 additions and 1328 deletions

View File

@@ -468,10 +468,24 @@ defmodule Mobilizon.Service.ActivityPub do
"""
@spec actor_data_from_actor_object(map()) :: {:ok, map()}
def actor_data_from_actor_object(data) when is_map(data) do
avatar =
data["icon"]["url"] &&
%{
"name" => data["icon"]["name"] || "avatar",
"url" => data["icon"]["url"]
}
banner =
data["image"]["url"] &&
%{
"name" => data["image"]["name"] || "banner",
"url" => data["image"]["url"]
}
actor_data = %{
url: data["id"],
avatar_url: data["icon"]["url"],
banner_url: data["image"]["url"],
avatar: avatar,
banner: banner,
name: data["name"],
preferred_username: data["preferredUsername"],
summary: data["summary"],
@@ -512,7 +526,7 @@ defmodule Mobilizon.Service.ActivityPub do
%Activity{
recipients: ["https://www.w3.org/ns/activitystreams#Public"],
actor: event.organizer_actor.url,
data: event |> make_event_data,
data: event |> Mobilizon.Service.ActivityPub.Converters.Event.model_to_as(),
local: local
}
end
@@ -523,7 +537,7 @@ defmodule Mobilizon.Service.ActivityPub do
%Activity{
recipients: ["https://www.w3.org/ns/activitystreams#Public"],
actor: comment.actor.url,
data: comment |> make_comment_data,
data: comment |> Mobilizon.Service.ActivityPub.Converters.Comment.model_to_as(),
local: local
}
end

View File

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

@@ -0,0 +1,47 @@
defmodule Mobilizon.Service.ActivityPub.Converters.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
@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
%{
"type" => String.to_existing_atom(object["type"]),
"preferred_username" => object["preferred_username"],
"summary" => object["summary"],
"url" => object["url"],
"name" => object["name"]
}
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,96 @@
defmodule Mobilizon.Service.ActivityPub.Converters.Comment do
@moduledoc """
Comment converter
This module allows to convert events from ActivityStream format to our own internal one, and back
"""
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Comment, as: CommentModel
alias Mobilizon.Events.Event
alias Mobilizon.Service.ActivityPub.Converter
alias Mobilizon.Service.ActivityPub
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
require Logger
@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
{:ok, %Actor{id: actor_id}} = Actors.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" => Routes.page_url(Endpoint, :comment, comment.uuid)
}
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,70 @@
defmodule Mobilizon.Service.ActivityPub.Converters.Event do
@moduledoc """
Event converter
This module allows to convert events from ActivityStream format to our own internal one, and back
"""
alias Mobilizon.Actors
alias Mobilizon.Media
alias Mobilizon.Media.Picture
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event, as: EventModel
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 {:ok, %Actor{id: actor_id}} <- Actors.get_actor_by_url(object["actor"]) do
picture_id =
with true <- Map.has_key?(object, "attachment"),
%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
%{
"title" => object["name"],
"description" => object["content"],
"organizer_actor_id" => actor_id,
"picture_id" => picture_id,
"begins_on" => object["begins_on"],
"category" => object["category"],
"url" => object["id"],
"uuid" => object["uuid"]
}
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
%{
"type" => "Event",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"title" => event.title,
"actor" => event.organizer_actor.url,
"uuid" => event.uuid,
"category" => event.category,
"summary" => event.description,
"publish_at" => (event.publish_at || event.inserted_at) |> DateTime.to_iso8601(),
"updated_at" => event.updated_at |> DateTime.to_iso8601(),
"id" => event.url
}
end
end

View File

@@ -15,9 +15,9 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
alias Mobilizon.Events.Comment
alias Mobilizon.Media.Picture
alias Mobilizon.Events
alias Mobilizon.Activity
alias Mobilizon.Service.ActivityPub
alias Ecto.Changeset
require Logger
alias MobilizonWeb.Router.Helpers, as: Routes
@@ -108,23 +108,6 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
Map.put_new_lazy(map, "published", &make_date/0)
end
@doc """
Converts an AP object data to our internal data structure
"""
def object_to_event_data(object) do
{:ok, %Actor{id: actor_id}} = Actors.get_actor_by_url(object["actor"])
%{
"title" => object["name"],
"description" => object["content"],
"organizer_actor_id" => actor_id,
"begins_on" => object["begins_on"],
"category" => object["category"],
"url" => object["id"],
"uuid" => object["uuid"]
}
end
@doc """
Inserts a full object if it is contained in an activity.
"""
@@ -135,7 +118,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"""
def insert_full_object(%{"object" => %{"type" => "Event"} = object_data})
when is_map(object_data) do
with object_data <- object_to_event_data(object_data),
with object_data <-
Mobilizon.Service.ActivityPub.Converters.Event.as_to_model_data(object_data),
{:ok, _} <- Events.create_event(object_data) do
:ok
end
@@ -155,60 +139,14 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"""
def insert_full_object(%{"object" => %{"type" => "Note"} = object_data})
when is_map(object_data) do
Logger.debug("Inserting full comment")
Logger.debug(inspect(object_data))
with {:ok, %Actor{id: actor_id}} <- Actors.get_or_fetch_by_url(object_data["actor"]) do
data = %{
"text" => object_data["content"],
"url" => object_data["id"],
"actor_id" => actor_id,
"in_reply_to_comment_id" => nil,
"event_id" => nil,
"uuid" => object_data["uuid"]
}
# We fetch the parent object
Logger.debug("We're fetching the parent object")
data =
if Map.has_key?(object_data, "inReplyTo") && object_data["inReplyTo"] != nil &&
object_data["inReplyTo"] != "" do
Logger.debug(fn -> "Object has inReplyTo #{object_data["inReplyTo"]}" end)
case ActivityPub.fetch_object_from_url(object_data["inReplyTo"]) do
# Reply to an event (Comment)
{:ok, %Event{id: id}} ->
Logger.debug("Parent object is an event")
data |> Map.put("event_id", id)
# Reply to a comment (Comment)
{:ok, %Comment{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 |> Comment.get_thread_id())
# Anything else is kind of a MP
{:error, object} ->
Logger.debug("Parent object is something we don't handle")
Logger.debug(inspect(object))
data
end
else
Logger.debug("No parent object for this comment")
data
end
with {:ok, _comment} <- Events.create_comment(data) do
:ok
else
err ->
Logger.error("Error while inserting a remote comment inside database")
Logger.error(inspect(err))
{:error, err}
end
with data <- Mobilizon.Service.ActivityPub.Converters.Comment.as_to_model_data(object_data),
{:ok, _comment} <- Events.create_comment(data) do
:ok
else
err ->
Logger.error("Error while inserting a remote comment inside database")
Logger.error(inspect(err))
{:error, err}
end
end
@@ -238,6 +176,43 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
# Repo.one(query)
# end
def make_picture_data(%Plug.Upload{} = picture) do
with {:ok, picture} <- MobilizonWeb.Upload.store(picture) do
picture
else
_ -> nil
end
end
def make_picture_data(%Picture{file: file} = _picture) do
%{
"type" => "Document",
"url" => [
%{
"type" => "Link",
"mediaType" => file.content_type,
"href" => file.url
}
],
"name" => file.name
}
end
def make_picture_data(%{picture: picture}) do
with {:ok, %{"url" => [%{"href" => url}]}} <- MobilizonWeb.Upload.store(picture.file),
{:ok, %Picture{file: _file} = pic} <-
Mobilizon.Media.create_picture(%{
"file" => %{
"url" => url,
"name" => picture.name
}
}) do
make_picture_data(pic)
end
end
def make_picture_data(nil), do: nil
@doc """
Make an AP event object from an set of values
"""
@@ -246,6 +221,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
String.t(),
String.t(),
String.t(),
map(),
list(),
list(),
map(),
@@ -256,7 +232,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
to,
title,
content_html,
# attachments,
picture \\ nil,
tags \\ [],
# _cw \\ nil,
cc \\ [],
@@ -266,14 +242,13 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
Logger.debug("Making event data")
uuid = Ecto.UUID.generate()
%{
res = %{
"type" => "Event",
"to" => to,
"cc" => cc,
"content" => content_html,
"name" => title,
# "summary" => cw,
# "attachment" => attachments,
"begins_on" => metadata.begins_on,
"category" => category,
"actor" => actor,
@@ -281,55 +256,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
"uuid" => uuid,
"tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
}
end
@spec make_event_data(Event.t(), list(String.t())) :: map()
def make_event_data(
%Event{} = event,
to \\ ["https://www.w3.org/ns/activitystreams#Public"]
) do
%{
"type" => "Event",
"to" => to,
"title" => event.title,
"actor" => event.organizer_actor.url,
"uuid" => event.uuid,
"category" => event.category,
"summary" => event.description,
"publish_at" => (event.publish_at || event.inserted_at) |> DateTime.to_iso8601(),
"updated_at" => event.updated_at |> DateTime.to_iso8601(),
"id" => Routes.page_url(Endpoint, :event, event.uuid)
}
end
@doc """
Make an AP comment object from an existing `Comment` structure.
"""
def make_comment_data(
%Comment{
text: text,
actor: actor,
uuid: uuid,
in_reply_to_comment: reply_to,
event: event
},
to \\ ["https://www.w3.org/ns/activitystreams#Public"]
) do
object = %{
"type" => "Note",
"to" => to,
"content" => text,
"actor" => actor.url,
"attributedTo" => actor.url,
"uuid" => uuid,
"id" => Routes.page_url(Endpoint, :comment, uuid)
}
if reply_to do
object |> Map.put("inReplyTo", reply_to.url || event.url)
else
object
end
if is_nil(picture), do: res, else: Map.put(res, "attachment", [make_picture_data(picture)])
end
@doc """

View File

@@ -74,12 +74,19 @@ defmodule Mobilizon.Service.Export.Feed do
|> Feed.generator("Mobilizon", uri: "https://joinmobilizon.org", version: version())
|> Feed.entries(Enum.map(events, &get_entry/1))
feed = if actor.avatar_url, do: Feed.icon(feed, actor.avatar_url), else: feed
feed =
if actor.avatar do
Feed.icon(feed, actor.avatar.url)
else
feed
end
feed =
if actor.banner_url,
do: Feed.logo(feed, actor.banner_url),
else: feed
if actor.banner do
Feed.logo(feed, actor.banner.url)
else
feed
end
feed
|> Feed.build()
@@ -113,7 +120,8 @@ defmodule Mobilizon.Service.Export.Feed do
@spec fetch_events_from_token(String.t()) :: String.t()
defp fetch_events_from_token(token) do
with %FeedToken{actor: actor, user: %User{} = user} <- Events.get_feed_token(token) do
with {:ok, _uuid} <- Ecto.UUID.cast(token),
%FeedToken{actor: actor, user: %User{} = user} <- Events.get_feed_token(token) do
case actor do
%Actor{} = actor ->
events = fetch_identity_going_to_events(actor)

View File

@@ -65,7 +65,7 @@ defmodule Mobilizon.Service.Formatter do
@link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@uri_schemes Application.get_env(:mobilizon, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
# # TODO: make it use something other than @link_regex

View File

@@ -3,14 +3,19 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Actors.Actor do
alias Mobilizon.Actors.Actor
def build_tags(%Actor{} = actor) do
[
tags = [
Tag.tag(:meta, property: "og:title", content: Actor.display_name_and_username(actor)),
Tag.tag(:meta, property: "og:url", content: actor.url),
Tag.tag(:meta, property: "og:description", content: actor.summary),
Tag.tag(:meta, property: "og:type", content: "profile"),
Tag.tag(:meta, property: "profile:username", content: actor.preferred_username),
Tag.tag(:meta, property: "og:image", content: actor.avatar_url),
Tag.tag(:meta, property: "twitter:card", content: "summary")
]
if is_nil(actor.avatar) do
tags
else
tags ++ [Tag.tag(:meta, property: "og:image", content: actor.avatar.url)]
end
end
end

View File

@@ -5,16 +5,28 @@ defimpl Mobilizon.Service.Metadata, for: Mobilizon.Events.Event do
alias MobilizonWeb.JsonLD.ObjectView
def build_tags(%Event{} = event) do
[
tags = [
Tag.tag(:meta, property: "og:title", content: event.title),
Tag.tag(:meta, property: "og:url", content: event.url),
Tag.tag(:meta, property: "og:description", content: event.description),
Tag.tag(:meta, property: "og:type", content: "website"),
Tag.tag(:meta, property: "og:image", content: event.thumbnail),
Tag.tag(:meta, property: "og:image", content: event.large_image),
Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"),
~s{<script type="application/ld+json">#{json(event)}</script>} |> HTML.raw()
Tag.tag(:meta, property: "og:type", content: "website")
]
tags =
if is_nil(event.picture) do
tags
else
tags ++
[
Tag.tag(:meta, property: "og:image", content: event.picture.file.url)
]
end
tags ++
[
Tag.tag(:meta, property: "twitter:card", content: "summary_large_image"),
~s{<script type="application/ld+json">#{json(event)}</script>} |> HTML.raw()
]
end
# Insert JSON-LD schema by hand because Tag.content_tag wants to escape it