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

@@ -33,6 +33,7 @@ defmodule Mobilizon.Actors.Actor do
alias Mobilizon.Users.User
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Events.{Event, FeedToken}
alias Mobilizon.Media.File
alias MobilizonWeb.Router.Helpers, as: Routes
alias MobilizonWeb.Endpoint
@@ -62,8 +63,6 @@ defmodule Mobilizon.Actors.Actor do
field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
field(:visibility, Mobilizon.Actors.ActorVisibilityEnum, default: :private)
field(:suspended, :boolean, default: false)
field(:avatar_url, :string)
field(:banner_url, :string)
# field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
has_many(:followers, Follower, foreign_key: :target_actor_id)
has_many(:followings, Follower, foreign_key: :actor_id)
@@ -71,6 +70,8 @@ defmodule Mobilizon.Actors.Actor do
many_to_many(:memberships, Actor, join_through: Member)
belongs_to(:user, User)
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
embeds_one(:avatar, File)
embeds_one(:banner, File)
timestamps()
end
@@ -93,11 +94,11 @@ defmodule Mobilizon.Actors.Actor do
:keys,
:manually_approves_followers,
:suspended,
:avatar_url,
:banner_url,
:user_id
])
|> build_urls()
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> unique_username_validator()
|> validate_required([:preferred_username, :keys, :suspended, :url])
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
@@ -119,10 +120,11 @@ defmodule Mobilizon.Actors.Actor do
:suspended,
:url,
:type,
:avatar_url,
:user_id
])
|> build_urls()
|> cast_embed(:avatar)
|> cast_embed(:banner)
# Needed because following constraint can't work for domain null values (local)
|> unique_username_validator()
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
@@ -152,9 +154,7 @@ defmodule Mobilizon.Actors.Actor do
:summary,
:preferred_username,
:keys,
:manually_approves_followers,
:avatar_url,
:banner_url
:manually_approves_followers
])
|> validate_required([
:url,
@@ -165,6 +165,8 @@ defmodule Mobilizon.Actors.Actor do
:preferred_username,
:keys
])
|> cast_embed(:avatar)
|> cast_embed(:banner)
# Needed because following constraint can't work for domain null values (local)
|> unique_username_validator()
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
@@ -193,10 +195,10 @@ defmodule Mobilizon.Actors.Actor do
:name,
:domain,
:summary,
:preferred_username,
:avatar_url,
:banner_url
:preferred_username
])
|> cast_embed(:avatar)
|> cast_embed(:banner)
|> build_urls(:Group)
|> put_change(:domain, nil)
|> put_change(:keys, Actors.create_keys())

View File

@@ -11,7 +11,7 @@ defmodule Mobilizon.Actors do
alias Mobilizon.Actors.{Actor, Bot, Member, Follower}
alias Mobilizon.Service.ActivityPub
# import Exgravatar
require Logger
@doc false
def data() do
@@ -57,9 +57,12 @@ defmodule Mobilizon.Actors do
end
# Get actor by ID and preload organized events, followers and followings
@spec get_actor_with_everything(integer()) :: Ecto.Query
@spec get_actor_with_everything(integer()) :: Ecto.Query.t()
defp do_get_actor_with_everything(id) do
from(a in Actor, where: a.id == ^id, preload: [:organized_events, :followers, :followings])
from(a in Actor,
where: a.id == ^id,
preload: [:organized_events, :followers, :followings]
)
end
@doc """
@@ -239,24 +242,29 @@ defmodule Mobilizon.Actors do
"""
@spec insert_or_update_actor(map(), boolean()) :: {:ok, Actor.t()}
def insert_or_update_actor(data, preload \\ false) do
cs = Actor.remote_actor_creation(data)
cs =
data
|> Actor.remote_actor_creation()
{:ok, actor} =
Repo.insert(
cs,
on_conflict: [
set: [
keys: data.keys,
avatar_url: data.avatar_url,
banner_url: data.banner_url,
name: data.name,
summary: data.summary
]
],
conflict_target: [:url]
)
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
with {:ok, actor} <-
Repo.insert(
cs,
on_conflict: [
set: [
keys: data.keys,
name: data.name,
summary: data.summary
]
],
conflict_target: [:url]
) do
actor = if preload, do: Repo.preload(actor, [:followers]), else: actor
{:ok, actor}
else
err ->
Logger.error(inspect(err))
{:error, err}
end
end
# def increase_event_count(%Actor{} = actor) do
@@ -291,7 +299,8 @@ defmodule Mobilizon.Actors do
{:error, :actor_not_found}
actor ->
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
actor = if preload, do: Repo.preload(actor, [:followers]), else: actor
{:ok, actor}
end
end
@@ -371,7 +380,11 @@ defmodule Mobilizon.Actors do
"""
@spec get_local_actor_by_name(String.t()) :: Actor.t() | nil
def get_local_actor_by_name(name) do
Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)))
Repo.one(
from(a in Actor,
where: a.preferred_username == ^name and is_nil(a.domain)
)
)
end
@doc """
@@ -435,6 +448,7 @@ defmodule Mobilizon.Actors do
{:ok, actor}
_ ->
Logger.error("Could not fetch by AP id")
{:error, "Could not fetch by AP id"}
end
end

View File

@@ -6,6 +6,9 @@ defmodule Mobilizon.Application do
import Cachex.Spec
alias Mobilizon.Service.Export.{Feed, ICalendar}
@name Mix.Project.config()[:name]
@version Mix.Project.config()[:version]
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
@@ -82,4 +85,13 @@ defmodule Mobilizon.Application do
MobilizonWeb.Endpoint.config_change(changed, removed)
:ok
end
def named_version, do: @name <> " " <> @version
def user_agent do
info =
"#{MobilizonWeb.Endpoint.url()} <#{Mobilizon.CommonConfig.get([:instance, :email], "")}>"
named_version() <> "; " <> info
end
end

View File

@@ -22,4 +22,45 @@ defmodule Mobilizon.CommonConfig do
defp instance_config(), do: Application.get_env(:mobilizon, :instance)
defp to_bool(v), do: v == true or v == "true" or v == "True"
def get(key), do: get(key, nil)
def get([key], default), do: get(key, default)
def get([parent_key | keys], default) do
case :mobilizon
|> Application.get_env(parent_key)
|> get_in(keys) do
nil -> default
any -> any
end
end
def get(key, default) do
Application.get_env(:mobilizon, key, default)
end
def get!(key) do
value = get(key, nil)
if value == nil do
raise("Missing configuration value: #{inspect(key)}")
else
value
end
end
def put([key], value), do: put(key, value)
def put([parent_key | keys], value) do
parent =
Application.get_env(:mobilizon, parent_key)
|> put_in(keys, value)
Application.put_env(:mobilizon, parent_key, parent)
end
def put(key, value) do
Application.put_env(:mobilizon, key, value)
end
end

View File

@@ -35,6 +35,7 @@ defmodule Mobilizon.Events.Event do
import Ecto.Changeset
alias Mobilizon.Events.{Event, Participant, Tag, Session, Track}
alias Mobilizon.Actors.Actor
alias Mobilizon.Media.Picture
alias Mobilizon.Addresses.Address
schema "events" do
@@ -48,8 +49,6 @@ defmodule Mobilizon.Events.Event do
field(:status, Mobilizon.Events.EventStatusEnum, default: :confirmed)
field(:visibility, Mobilizon.Events.EventVisibilityEnum, default: :public)
field(:join_options, Mobilizon.Events.JoinOptionsEnum, default: :free)
field(:thumbnail, :string)
field(:large_image, :string)
field(:publish_at, :utc_datetime)
field(:uuid, Ecto.UUID, default: Ecto.UUID.generate())
field(:online_address, :string)
@@ -62,6 +61,7 @@ defmodule Mobilizon.Events.Event do
has_many(:tracks, Track)
has_many(:sessions, Session)
belongs_to(:physical_address, Address)
belongs_to(:picture, Picture)
timestamps(type: :utc_datetime)
end
@@ -80,12 +80,11 @@ defmodule Mobilizon.Events.Event do
:category,
:status,
:visibility,
:thumbnail,
:large_image,
:publish_at,
:online_address,
:phone_address,
:uuid
:uuid,
:picture_id
])
|> cast_assoc(:tags)
|> cast_assoc(:physical_address)

View File

@@ -178,7 +178,8 @@ defmodule Mobilizon.Events do
:tracks,
:tags,
:participants,
:physical_address
:physical_address,
:picture
]
)
|> Repo.one()
@@ -692,7 +693,7 @@ defmodule Mobilizon.Events do
on: p.actor_id == a.id,
on: p.event_id == e.id,
where: a.id == ^id and p.role != ^:not_approved,
preload: [:tags]
preload: [:picture, :tags]
)
|> paginate(page, limit)
)
@@ -1239,11 +1240,7 @@ defmodule Mobilizon.Events do
"""
def get_feed_token(token) do
from(
tk in FeedToken,
where: tk.token == ^token,
preload: [:actor, :user]
)
from(ftk in FeedToken, where: ftk.token == ^token, preload: [:actor, :user])
|> Repo.one()
end

115
lib/mobilizon/media.ex Normal file
View File

@@ -0,0 +1,115 @@
defmodule Mobilizon.Media do
@moduledoc """
The Media context.
"""
import Ecto.Query, warn: false
alias Mobilizon.Repo
alias Mobilizon.Media.Picture
@doc false
def data() do
Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2)
end
@doc false
def query(queryable, _params) do
queryable
end
@doc """
Gets a single picture.
Raises `Ecto.NoResultsError` if the Picture does not exist.
## Examples
iex> get_picture!(123)
%Picture{}
iex> get_picture!(456)
** (Ecto.NoResultsError)
"""
def get_picture!(id), do: Repo.get!(Picture, id)
def get_picture(id), do: Repo.get(Picture, id)
@doc """
Get a picture by it's URL
"""
@spec get_picture_by_url(String.t()) :: Picture.t() | nil
def get_picture_by_url(url) do
from(
p in Picture,
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
)
|> Repo.one()
end
@doc """
Creates a picture.
## Examples
iex> create_picture(%{field: value})
{:ok, %Picture{}}
iex> create_picture(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_picture(attrs \\ %{}) do
%Picture{}
|> Picture.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a picture.
## Examples
iex> update_picture(picture, %{field: new_value})
{:ok, %Picture{}}
iex> update_picture(picture, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_picture(%Picture{} = picture, attrs) do
picture
|> Picture.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a Picture.
## Examples
iex> delete_picture(picture)
{:ok, %Picture{}}
iex> delete_picture(picture)
{:error, %Ecto.Changeset{}}
"""
def delete_picture(%Picture{} = picture) do
Repo.delete(picture)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking picture changes.
## Examples
iex> change_picture(picture)
%Ecto.Changeset{source: %Picture{}}
"""
def change_picture(%Picture{} = picture) do
Picture.changeset(picture, %{})
end
end

View File

@@ -0,0 +1,22 @@
defmodule Mobilizon.Media.File do
@moduledoc """
Represents a file entity
"""
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field(:name, :string)
field(:url, :string)
field(:content_type, :string)
timestamps()
end
@doc false
def changeset(picture, attrs) do
picture
|> cast(attrs, [:name, :url, :content_type])
|> validate_required([:name, :url])
end
end

View File

@@ -0,0 +1,21 @@
defmodule Mobilizon.Media.Picture do
@moduledoc """
Represents a picture entity
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Media.File
schema "pictures" do
embeds_one(:file, File, on_replace: :update)
timestamps()
end
@doc false
def changeset(picture, attrs) do
picture
|> cast(attrs, [])
|> cast_embed(:file)
end
end

121
lib/mobilizon/mime.ex Normal file
View File

@@ -0,0 +1,121 @@
# 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/mime.ex
defmodule Mobilizon.MIME do
@moduledoc """
Returns the mime-type of a binary and optionally a normalized file-name.
"""
@default "application/octet-stream"
@read_bytes 35
@spec file_mime_type(String.t()) ::
{:ok, content_type :: String.t(), filename :: String.t()} | {:error, any()} | :error
def file_mime_type(path, filename) do
with {:ok, content_type} <- file_mime_type(path),
filename <- fix_extension(filename, content_type) do
{:ok, content_type, filename}
end
end
@spec file_mime_type(String.t()) :: {:ok, String.t()} | {:error, any()} | :error
def file_mime_type(filename) do
File.open(filename, [:read], fn f ->
check_mime_type(IO.binread(f, @read_bytes))
end)
end
def bin_mime_type(binary, filename) do
with {:ok, content_type} <- bin_mime_type(binary),
filename <- fix_extension(filename, content_type) do
{:ok, content_type, filename}
end
end
@spec bin_mime_type(binary()) :: {:ok, String.t()} | :error
def bin_mime_type(<<head::binary-size(@read_bytes), _::binary>>) do
{:ok, check_mime_type(head)}
end
def bin_mime_type(_), do: :error
def mime_type(<<_::binary>>), do: {:ok, @default}
defp fix_extension(filename, content_type) do
parts = String.split(filename, ".")
new_filename =
if length(parts) > 1 do
Enum.drop(parts, -1) |> Enum.join(".")
else
Enum.join(parts)
end
cond do
content_type == "application/octet-stream" ->
filename
ext = List.first(MIME.extensions(content_type)) ->
new_filename <> "." <> ext
true ->
Enum.join([new_filename, String.split(content_type, "/") |> List.last()], ".")
end
end
defp check_mime_type(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>) do
"image/png"
end
defp check_mime_type(<<0x47, 0x49, 0x46, 0x38, _, 0x61, _::binary>>) do
"image/gif"
end
defp check_mime_type(<<0xFF, 0xD8, 0xFF, _::binary>>) do
"image/jpeg"
end
defp check_mime_type(<<0x1A, 0x45, 0xDF, 0xA3, _::binary>>) do
"video/webm"
end
defp check_mime_type(<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70, _::binary>>) do
"video/mp4"
end
defp check_mime_type(<<0x49, 0x44, 0x33, _::binary>>) do
"audio/mpeg"
end
defp check_mime_type(<<255, 251, _, 68, 0, 0, 0, 0, _::binary>>) do
"audio/mpeg"
end
defp check_mime_type(
<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::size(160), 0x80, 0x74, 0x68, 0x65,
0x6F, 0x72, 0x61, _::binary>>
) do
"video/ogg"
end
defp check_mime_type(<<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00, _::binary>>) do
"audio/ogg"
end
defp check_mime_type(<<"RIFF", _::binary-size(4), "WAVE", _::binary>>) do
"audio/wav"
end
defp check_mime_type(<<"RIFF", _::binary-size(4), "WEBP", _::binary>>) do
"image/webp"
end
defp check_mime_type(<<"RIFF", _::binary-size(4), "AVI.", _::binary>>) do
"video/avi"
end
defp check_mime_type(_) do
@default
end
end