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:
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
115
lib/mobilizon/media.ex
Normal 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
|
||||
22
lib/mobilizon/media/file.ex
Normal file
22
lib/mobilizon/media/file.ex
Normal 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
|
||||
21
lib/mobilizon/media/picture.ex
Normal file
21
lib/mobilizon/media/picture.ex
Normal 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
121
lib/mobilizon/mime.ex
Normal 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
|
||||
@@ -26,8 +26,9 @@ defmodule MobilizonWeb.API.Events do
|
||||
title <- String.trim(title),
|
||||
mentions <- Formatter.parse_mentions(description),
|
||||
visibility <- Map.get(args, :visibility, "public"),
|
||||
{to, cc} <- to_for_actor_and_mentions(actor, mentions, nil, visibility),
|
||||
{to, cc} <- to_for_actor_and_mentions(actor, mentions, nil, Atom.to_string(visibility)),
|
||||
tags <- Formatter.parse_tags(description),
|
||||
picture <- Map.get(args, :picture, nil),
|
||||
content_html <-
|
||||
make_content_html(
|
||||
description,
|
||||
@@ -41,6 +42,7 @@ defmodule MobilizonWeb.API.Events do
|
||||
to,
|
||||
title,
|
||||
content_html,
|
||||
picture,
|
||||
tags,
|
||||
cc,
|
||||
%{begins_on: begins_on},
|
||||
|
||||
@@ -5,11 +5,9 @@
|
||||
|
||||
defmodule MobilizonWeb.ActivityPubController do
|
||||
use MobilizonWeb, :controller
|
||||
alias Mobilizon.{Actors, Actors.Actor, Events}
|
||||
alias Mobilizon.Events.{Event, Comment}
|
||||
alias MobilizonWeb.ActivityPub.{ObjectView, ActorView}
|
||||
alias Mobilizon.{Actors, Actors.Actor}
|
||||
alias MobilizonWeb.ActivityPub.ActorView
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
alias Mobilizon.Service.ActivityPub.Utils
|
||||
alias Mobilizon.Service.Federator
|
||||
|
||||
require Logger
|
||||
|
||||
45
lib/mobilizon_web/controllers/media_proxy_controller.ex
Normal file
45
lib/mobilizon_web/controllers/media_proxy_controller.ex
Normal file
@@ -0,0 +1,45 @@
|
||||
# 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/web/media_proxy/controller.ex
|
||||
|
||||
defmodule MobilizonWeb.MediaProxyController do
|
||||
use MobilizonWeb, :controller
|
||||
alias MobilizonWeb.ReverseProxy
|
||||
alias MobilizonWeb.MediaProxy
|
||||
|
||||
@default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]]
|
||||
|
||||
def remote(conn, %{"sig" => sig64, "url" => url64} = params) do
|
||||
with config <- Mobilizon.CommonConfig.get([:media_proxy], []),
|
||||
true <- Keyword.get(config, :enabled, false),
|
||||
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
|
||||
:ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do
|
||||
ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts))
|
||||
else
|
||||
false ->
|
||||
send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
|
||||
|
||||
{:error, :invalid_signature} ->
|
||||
send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
|
||||
|
||||
{:wrong_filename, filename} ->
|
||||
redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
|
||||
end
|
||||
end
|
||||
|
||||
def filename_matches(has_filename, path, url) do
|
||||
filename =
|
||||
url
|
||||
|> MediaProxy.filename()
|
||||
|> URI.decode()
|
||||
|
||||
path = URI.decode(path)
|
||||
|
||||
if has_filename && filename && Path.basename(path) != filename do
|
||||
{:wrong_filename, filename}
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,9 +4,7 @@ defmodule MobilizonWeb.PageController do
|
||||
"""
|
||||
use MobilizonWeb, :controller
|
||||
alias Mobilizon.Actors
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Comment}
|
||||
|
||||
action_fallback(MobilizonWeb.FallbackController)
|
||||
|
||||
|
||||
@@ -9,12 +9,7 @@ defmodule MobilizonWeb.Endpoint do
|
||||
# You should set gzip to true if you are running phoenix.digest
|
||||
# when deploying your static files in production.
|
||||
|
||||
plug(
|
||||
Plug.Static,
|
||||
at: "/uploads",
|
||||
from: "./uploads",
|
||||
gzip: false
|
||||
)
|
||||
plug(MobilizonWeb.Plugs.UploadedMedia)
|
||||
|
||||
plug(
|
||||
Plug.Static,
|
||||
@@ -38,7 +33,7 @@ defmodule MobilizonWeb.Endpoint do
|
||||
|
||||
plug(
|
||||
Plug.Parsers,
|
||||
parsers: [:urlencoded, :multipart, :json],
|
||||
parsers: [:urlencoded, :multipart, :json, Absinthe.Plug.Parser],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Jason
|
||||
)
|
||||
@@ -57,22 +52,4 @@ defmodule MobilizonWeb.Endpoint do
|
||||
)
|
||||
|
||||
plug(MobilizonWeb.Router)
|
||||
|
||||
@doc """
|
||||
Callback invoked for dynamically configuring the endpoint.
|
||||
|
||||
It receives the endpoint configuration and checks if
|
||||
configuration should be loaded from the system environment.
|
||||
"""
|
||||
def init(_key, config) do
|
||||
if config[:load_from_system_env] do
|
||||
port =
|
||||
System.get_env("MOBILIZON_INSTANCE_PORT") ||
|
||||
raise "expected the MOBILIZON_INSTANCE_PORT environment variable to be set"
|
||||
|
||||
{:ok, Keyword.put(config, :http, [:inet6, port: port])}
|
||||
else
|
||||
{:ok, config}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
86
lib/mobilizon_web/media_proxy.ex
Normal file
86
lib/mobilizon_web/media_proxy.ex
Normal file
@@ -0,0 +1,86 @@
|
||||
# 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/web/media_proxy/media_proxy.ex
|
||||
|
||||
defmodule MobilizonWeb.MediaProxy do
|
||||
@moduledoc """
|
||||
Handles proxifying media files
|
||||
"""
|
||||
@base64_opts [padding: false]
|
||||
|
||||
def url(nil), do: nil
|
||||
|
||||
def url(""), do: nil
|
||||
|
||||
def url("/" <> _ = url), do: url
|
||||
|
||||
def url(url) do
|
||||
config = Application.get_env(:mobilizon, :media_proxy, [])
|
||||
|
||||
if !Keyword.get(config, :enabled, false) or
|
||||
String.starts_with?(url, MobilizonWeb.Endpoint.url()) do
|
||||
url
|
||||
else
|
||||
encode_url(url)
|
||||
end
|
||||
end
|
||||
|
||||
def encode_url(url) do
|
||||
secret = Application.get_env(:mobilizon, MobilizonWeb.Endpoint)[:secret_key_base]
|
||||
|
||||
# Must preserve `%2F` for compatibility with S3
|
||||
# https://git.pleroma.social/pleroma/pleroma/issues/580
|
||||
replacement = get_replacement(url, ":2F:")
|
||||
|
||||
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
|
||||
base64 =
|
||||
url
|
||||
|> String.replace("%2F", replacement)
|
||||
|> URI.decode()
|
||||
|> URI.encode()
|
||||
|> String.replace(replacement, "%2F")
|
||||
|> Base.url_encode64(@base64_opts)
|
||||
|
||||
sig = :crypto.hmac(:sha, secret, base64)
|
||||
sig64 = sig |> Base.url_encode64(@base64_opts)
|
||||
|
||||
build_url(sig64, base64, filename(url))
|
||||
end
|
||||
|
||||
def decode_url(sig, url) do
|
||||
secret = Application.get_env(:mobilizon, MobilizonWeb.Endpoint)[:secret_key_base]
|
||||
sig = Base.url_decode64!(sig, @base64_opts)
|
||||
local_sig = :crypto.hmac(:sha, secret, url)
|
||||
|
||||
if local_sig == sig do
|
||||
{:ok, Base.url_decode64!(url, @base64_opts)}
|
||||
else
|
||||
{:error, :invalid_signature}
|
||||
end
|
||||
end
|
||||
|
||||
def filename(url_or_path) do
|
||||
if path = URI.parse(url_or_path).path, do: Path.basename(path)
|
||||
end
|
||||
|
||||
def build_url(sig_base64, url_base64, filename \\ nil) do
|
||||
[
|
||||
Mobilizon.CommonConfig.get([:media_proxy, :base_url], MobilizonWeb.Endpoint.url()),
|
||||
"proxy",
|
||||
sig_base64,
|
||||
url_base64,
|
||||
filename
|
||||
]
|
||||
|> Enum.filter(fn value -> value end)
|
||||
|> Path.join()
|
||||
end
|
||||
|
||||
defp get_replacement(url, replacement) do
|
||||
if String.contains?(url, replacement) do
|
||||
get_replacement(url, replacement <> replacement)
|
||||
else
|
||||
replacement
|
||||
end
|
||||
end
|
||||
end
|
||||
95
lib/mobilizon_web/plugs/uploaded_media.ex
Normal file
95
lib/mobilizon_web/plugs/uploaded_media.ex
Normal file
@@ -0,0 +1,95 @@
|
||||
# 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/plugs/uploaded_media.ex
|
||||
|
||||
defmodule MobilizonWeb.Plugs.UploadedMedia do
|
||||
@moduledoc """
|
||||
Serves uploaded media files
|
||||
"""
|
||||
|
||||
import Plug.Conn
|
||||
require Logger
|
||||
|
||||
@behaviour Plug
|
||||
# no slashes
|
||||
@path "media"
|
||||
|
||||
def init(_opts) do
|
||||
static_plug_opts =
|
||||
[]
|
||||
|> Keyword.put(:from, "__unconfigured_media_plug")
|
||||
|> Keyword.put(:at, "/__unconfigured_media_plug")
|
||||
|> Plug.Static.init()
|
||||
|
||||
%{static_plug_opts: static_plug_opts}
|
||||
end
|
||||
|
||||
def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
|
||||
conn =
|
||||
case fetch_query_params(conn) do
|
||||
%{query_params: %{"name" => name}} = conn ->
|
||||
name = String.replace(name, "\"", "\\\"")
|
||||
|
||||
conn
|
||||
|> put_resp_header("content-disposition", "filename=\"#{name}\"")
|
||||
|
||||
conn ->
|
||||
conn
|
||||
end
|
||||
|
||||
config = Mobilizon.CommonConfig.get([MobilizonWeb.Upload])
|
||||
|
||||
with uploader <- Keyword.fetch!(config, :uploader),
|
||||
proxy_remote = Keyword.get(config, :proxy_remote, false),
|
||||
{:ok, get_method} <- uploader.get_file(file) do
|
||||
get_media(conn, get_method, proxy_remote, opts)
|
||||
else
|
||||
_ ->
|
||||
conn
|
||||
|> send_resp(500, "Failed")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _opts), do: conn
|
||||
|
||||
defp get_media(conn, {:static_dir, directory}, _, opts) do
|
||||
static_opts =
|
||||
Map.get(opts, :static_plug_opts)
|
||||
|> Map.put(:at, [@path])
|
||||
|> Map.put(:from, directory)
|
||||
|
||||
conn = Plug.Static.call(conn, static_opts)
|
||||
|
||||
if conn.halted do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> send_resp(404, "Not found")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
defp get_media(conn, {:url, url}, true, _) do
|
||||
conn
|
||||
|> MobilizonWeb.ReverseProxy.call(
|
||||
url,
|
||||
Mobilizon.CommonConfig.get([Mobilizon.Upload, :proxy_opts], [])
|
||||
)
|
||||
end
|
||||
|
||||
defp get_media(conn, {:url, url}, _, _) do
|
||||
conn
|
||||
|> Phoenix.Controller.redirect(external: url)
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp get_media(conn, unknown, _, _) do
|
||||
Logger.error("#{__MODULE__}: Unknown get startegy: #{inspect(unknown)}")
|
||||
|
||||
conn
|
||||
|> send_resp(500, "Internal Error")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,7 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
alias Mobilizon.Activity
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.Events.{Event, Participant}
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Users.User
|
||||
|
||||
@@ -185,16 +186,27 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
@doc """
|
||||
Create an event
|
||||
"""
|
||||
def create_event(_parent, args, %{context: %{current_user: _user}}) do
|
||||
with {:ok, %Activity{data: %{"object" => %{"type" => "Event"} = object}}} <-
|
||||
def create_event(_parent, args, %{context: %{current_user: _user}} = _resolution) do
|
||||
with {:ok, args} <- save_attached_picture(args),
|
||||
{:ok, %Activity{data: %{"object" => %{"type" => "Event"} = object}}} <-
|
||||
MobilizonWeb.API.Events.create_event(args) do
|
||||
{:ok,
|
||||
%Event{
|
||||
title: object["name"],
|
||||
description: object["content"],
|
||||
uuid: object["uuid"],
|
||||
url: object["id"]
|
||||
}}
|
||||
res = %{
|
||||
title: object["name"],
|
||||
description: object["content"],
|
||||
uuid: object["uuid"],
|
||||
url: object["id"]
|
||||
}
|
||||
|
||||
res =
|
||||
if Map.has_key?(object, "attachment"),
|
||||
do:
|
||||
Map.put(res, :picture, %{
|
||||
name: object["attachment"] |> hd() |> Map.get("name"),
|
||||
url: object["attachment"] |> hd() |> Map.get("url") |> hd() |> Map.get("href")
|
||||
}),
|
||||
else: res
|
||||
|
||||
{:ok, res}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -202,6 +214,22 @@ defmodule MobilizonWeb.Resolvers.Event do
|
||||
{:error, "You need to be logged-in to create events"}
|
||||
end
|
||||
|
||||
# If we have an attached picture, just transmit it. It will be handled by
|
||||
# Mobilizon.Service.ActivityPub.Utils.make_picture_data/1
|
||||
@spec save_attached_picture(map()) :: {:ok, map()}
|
||||
defp save_attached_picture(%{picture: %{picture: %Plug.Upload{} = _picture}} = args), do: args
|
||||
|
||||
# Otherwise if we use a previously uploaded picture we need to fetch it from database
|
||||
@spec save_attached_picture(map()) :: {:ok, map()}
|
||||
defp save_attached_picture(%{picture: %{picture_id: picture_id}} = args) do
|
||||
with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do
|
||||
{:ok, Map.put(args, :picture, picture)}
|
||||
end
|
||||
end
|
||||
|
||||
@spec save_attached_picture(map()) :: {:ok, map()}
|
||||
defp save_attached_picture(args), do: {:ok, args}
|
||||
|
||||
@doc """
|
||||
Delete an event
|
||||
"""
|
||||
|
||||
@@ -47,12 +47,17 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
@doc """
|
||||
This function is used to create more identities from an existing user
|
||||
"""
|
||||
def create_person(_parent, %{preferred_username: _preferred_username} = args, %{
|
||||
context: %{current_user: user}
|
||||
}) do
|
||||
def create_person(
|
||||
_parent,
|
||||
%{preferred_username: _preferred_username} = args,
|
||||
%{
|
||||
context: %{current_user: user}
|
||||
} = _resolution
|
||||
) do
|
||||
args = Map.put(args, :user_id, user.id)
|
||||
|
||||
with {:ok, %Actor{} = new_person} <- Actors.new_person(args) do
|
||||
with args <- save_attached_pictures(args),
|
||||
{:ok, %Actor{} = new_person} <- Actors.new_person(args) do
|
||||
{:ok, new_person}
|
||||
end
|
||||
end
|
||||
@@ -64,6 +69,21 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
{:error, "You need to be logged-in to create a new identity"}
|
||||
end
|
||||
|
||||
defp save_attached_pictures(args) do
|
||||
Enum.reduce([:avatar, :banner], args, fn key, args ->
|
||||
if Map.has_key?(args, key) do
|
||||
pic = args[key][:picture]
|
||||
|
||||
with {:ok, %{"name" => name, "url" => [%{"href" => url, "mediaType" => content_type}]}} <-
|
||||
MobilizonWeb.Upload.store(pic.file, type: key, description: pic.alt) do
|
||||
Map.put(args, key, %{"name" => name, "url" => url, "mediaType" => content_type})
|
||||
end
|
||||
else
|
||||
args
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
This function is used to register a person afterwards the user has been created (but not activated)
|
||||
"""
|
||||
@@ -71,6 +91,7 @@ defmodule MobilizonWeb.Resolvers.Person do
|
||||
with {:ok, %User{} = user} <- Users.get_user_by_email(args.email),
|
||||
{:no_actor, nil} <- {:no_actor, Users.get_actor_for_user(user)},
|
||||
args <- Map.put(args, :user_id, user.id),
|
||||
args <- save_attached_pictures(args),
|
||||
{:ok, %Actor{} = new_person} <- Actors.new_person(args) do
|
||||
{:ok, new_person}
|
||||
else
|
||||
|
||||
64
lib/mobilizon_web/resolvers/picture.ex
Normal file
64
lib/mobilizon_web/resolvers/picture.ex
Normal file
@@ -0,0 +1,64 @@
|
||||
defmodule MobilizonWeb.Resolvers.Picture do
|
||||
@moduledoc """
|
||||
Handles the picture-related GraphQL calls
|
||||
"""
|
||||
alias Mobilizon.Media
|
||||
alias Mobilizon.Media.Picture
|
||||
|
||||
@doc """
|
||||
Get picture for an event's pic
|
||||
"""
|
||||
def picture(%{picture_id: picture_id} = _parent, _args, _resolution) do
|
||||
with {:ok, picture} <- do_fetch_picture(picture_id) do
|
||||
{:ok, picture}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get picture for an event that has an attached
|
||||
|
||||
See MobilizonWeb.Resolvers.Event.create_event/3
|
||||
"""
|
||||
def picture(%{picture: picture} = _parent, _args, _resolution) do
|
||||
{:ok, picture}
|
||||
end
|
||||
|
||||
def picture(_parent, %{id: picture_id}, _resolution), do: do_fetch_picture(picture_id)
|
||||
|
||||
def picture(_parent, _args, _resolution) do
|
||||
{:ok, nil}
|
||||
end
|
||||
|
||||
@spec do_fetch_picture(nil) :: {:error, nil}
|
||||
defp do_fetch_picture(nil), do: {:error, nil}
|
||||
|
||||
@spec do_fetch_picture(String.t()) :: {:ok, Picture.t()} | {:error, :not_found}
|
||||
defp do_fetch_picture(picture_id) do
|
||||
with %Picture{id: id, file: file} = _pic <- Media.get_picture(picture_id) do
|
||||
{:ok, %{name: file.name, url: file.url, id: id}}
|
||||
else
|
||||
_err ->
|
||||
{:error, "Picture with ID #{picture_id} was not found"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec upload_picture(map(), map(), map()) :: {:ok, Picture.t()} | {:error, any()}
|
||||
def upload_picture(_parent, %{file: %Plug.Upload{} = file} = args, %{
|
||||
context: %{
|
||||
current_user: _user
|
||||
}
|
||||
}) do
|
||||
with {:ok, %{"url" => [%{"href" => url}]}} <- MobilizonWeb.Upload.store(file),
|
||||
args <- Map.put(args, :url, url),
|
||||
{:ok, picture = %Picture{}} <- Media.create_picture(%{"file" => args}) do
|
||||
{:ok, %{name: picture.file.name, url: picture.file.url, id: picture.id}}
|
||||
else
|
||||
err ->
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
def upload_picture(_parent, _args, _resolution) do
|
||||
{:error, "You need to login to upload a picture"}
|
||||
end
|
||||
end
|
||||
382
lib/mobilizon_web/reverse_proxy.ex
Normal file
382
lib/mobilizon_web/reverse_proxy.ex
Normal file
@@ -0,0 +1,382 @@
|
||||
# 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/reverse_proxy.ex
|
||||
|
||||
defmodule MobilizonWeb.ReverseProxy do
|
||||
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
|
||||
~w(if-unmodified-since if-none-match if-range range)
|
||||
@resp_cache_headers ~w(etag date last-modified cache-control)
|
||||
@keep_resp_headers @resp_cache_headers ++
|
||||
~w(content-type content-disposition content-encoding content-range) ++
|
||||
~w(accept-ranges vary)
|
||||
@default_cache_control_header "public, max-age=1209600"
|
||||
@valid_resp_codes [200, 206, 304]
|
||||
@max_read_duration :timer.seconds(30)
|
||||
@max_body_length :infinity
|
||||
@methods ~w(GET HEAD)
|
||||
|
||||
@moduledoc """
|
||||
A reverse proxy.
|
||||
|
||||
MobilizonWeb.ReverseProxy.call(conn, url, options)
|
||||
|
||||
It is not meant to be added into a plug pipeline, but to be called from another plug or controller.
|
||||
|
||||
Supports `#{inspect(@methods)}` HTTP methods, and only allows `#{inspect(@valid_resp_codes)}` status codes.
|
||||
|
||||
Responses are chunked to the client while downloading from the upstream.
|
||||
|
||||
Some request / responses headers are preserved:
|
||||
|
||||
* request: `#{inspect(@keep_req_headers)}`
|
||||
* response: `#{inspect(@keep_resp_headers)}`
|
||||
|
||||
If no caching headers (`#{inspect(@resp_cache_headers)}`) are returned by upstream, `cache-control` will be
|
||||
set to `#{inspect(@default_cache_control_header)}`.
|
||||
|
||||
Options:
|
||||
|
||||
* `redirect_on_failure` (default `false`). Redirects the client to the real remote URL if there's any HTTP
|
||||
errors. Any error during body processing will not be redirected as the response is chunked. This may expose
|
||||
remote URL, clients IPs, ….
|
||||
|
||||
* `max_body_length` (default `#{inspect(@max_body_length)}`): limits the content length to be approximately the
|
||||
specified length. It is validated with the `content-length` header and also verified when proxying.
|
||||
|
||||
* `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to
|
||||
read from the remote upstream.
|
||||
|
||||
* `inline_content_types`:
|
||||
* `true` will not alter `content-disposition` (up to the upstream),
|
||||
* `false` will add `content-disposition: attachment` to any request,
|
||||
* a list of whitelisted content types
|
||||
|
||||
* `keep_user_agent` will forward the client's user-agent to the upstream. This may be useful if the upstream is
|
||||
doing content transformation (encoding, …) depending on the request.
|
||||
|
||||
* `req_headers`, `resp_headers` additional headers.
|
||||
|
||||
* `http`: options for [hackney](https://github.com/benoitc/hackney).
|
||||
|
||||
"""
|
||||
@hackney Application.get_env(:mobilizon, :hackney, :hackney)
|
||||
@httpoison Application.get_env(:mobilizon, :httpoison, HTTPoison)
|
||||
|
||||
@default_hackney_options []
|
||||
|
||||
@inline_content_types [
|
||||
"image/gif",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/svg+xml",
|
||||
"audio/mpeg",
|
||||
"audio/mp3",
|
||||
"video/webm",
|
||||
"video/mp4",
|
||||
"video/quicktime"
|
||||
]
|
||||
|
||||
require Logger
|
||||
import Plug.Conn
|
||||
|
||||
@type option() ::
|
||||
{:keep_user_agent, boolean}
|
||||
| {:max_read_duration, :timer.time() | :infinity}
|
||||
| {:max_body_length, non_neg_integer() | :infinity}
|
||||
| {:http, []}
|
||||
| {:req_headers, [{String.t(), String.t()}]}
|
||||
| {:resp_headers, [{String.t(), String.t()}]}
|
||||
| {:inline_content_types, boolean() | [String.t()]}
|
||||
| {:redirect_on_failure, boolean()}
|
||||
|
||||
@spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t()
|
||||
def call(_conn, _url, _opts \\ [])
|
||||
|
||||
def call(conn = %{method: method}, url, opts) when method in @methods do
|
||||
hackney_opts =
|
||||
@default_hackney_options
|
||||
|> Keyword.merge(Keyword.get(opts, :http, []))
|
||||
|> @httpoison.process_request_options()
|
||||
|
||||
req_headers = build_req_headers(conn.req_headers, opts)
|
||||
|
||||
opts =
|
||||
if filename = MobilizonWeb.MediaProxy.filename(url) do
|
||||
Keyword.put_new(opts, :attachment_name, filename)
|
||||
else
|
||||
opts
|
||||
end
|
||||
|
||||
with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
|
||||
:ok <- header_length_constraint(headers, Keyword.get(opts, :max_body_length)) do
|
||||
response(conn, client, url, code, headers, opts)
|
||||
else
|
||||
{:ok, code, headers} ->
|
||||
head_response(conn, url, code, headers, opts)
|
||||
|> halt()
|
||||
|
||||
{:error, {:invalid_http_response, code}} ->
|
||||
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}")
|
||||
|
||||
conn
|
||||
|> error_or_redirect(
|
||||
url,
|
||||
code,
|
||||
"Request failed: " <> Plug.Conn.Status.reason_phrase(code),
|
||||
opts
|
||||
)
|
||||
|> halt()
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}")
|
||||
|
||||
conn
|
||||
|> error_or_redirect(url, 500, "Request failed", opts)
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _, _) do
|
||||
conn
|
||||
|> send_resp(400, Plug.Conn.Status.reason_phrase(400))
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp request(method, url, headers, hackney_opts) do
|
||||
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
|
||||
method = method |> String.downcase() |> String.to_existing_atom()
|
||||
|
||||
case @hackney.request(method, url, headers, "", hackney_opts) do
|
||||
{:ok, code, headers, client} when code in @valid_resp_codes ->
|
||||
{:ok, code, downcase_headers(headers), client}
|
||||
|
||||
{:ok, code, headers} when code in @valid_resp_codes ->
|
||||
{:ok, code, downcase_headers(headers)}
|
||||
|
||||
{:ok, code, _, _} ->
|
||||
{:error, {:invalid_http_response, code}}
|
||||
|
||||
{:error, error} ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp response(conn, client, url, status, headers, opts) do
|
||||
result =
|
||||
conn
|
||||
|> put_resp_headers(build_resp_headers(headers, opts))
|
||||
|> send_chunked(status)
|
||||
|> chunk_reply(client, opts)
|
||||
|
||||
case result do
|
||||
{:ok, conn} ->
|
||||
halt(conn)
|
||||
|
||||
{:error, :closed, conn} ->
|
||||
:hackney.close(client)
|
||||
halt(conn)
|
||||
|
||||
{:error, error, conn} ->
|
||||
Logger.warn(
|
||||
"#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}"
|
||||
)
|
||||
|
||||
:hackney.close(client)
|
||||
halt(conn)
|
||||
end
|
||||
end
|
||||
|
||||
defp chunk_reply(conn, client, opts) do
|
||||
chunk_reply(conn, client, opts, 0, 0)
|
||||
end
|
||||
|
||||
defp chunk_reply(conn, client, opts, sent_so_far, duration) do
|
||||
with {:ok, duration} <-
|
||||
check_read_duration(
|
||||
duration,
|
||||
Keyword.get(opts, :max_read_duration, @max_read_duration)
|
||||
),
|
||||
{:ok, data} <- @hackney.stream_body(client),
|
||||
{:ok, duration} <- increase_read_duration(duration),
|
||||
sent_so_far = sent_so_far + byte_size(data),
|
||||
:ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),
|
||||
{:ok, conn} <- chunk(conn, data) do
|
||||
chunk_reply(conn, client, opts, sent_so_far, duration)
|
||||
else
|
||||
:done -> {:ok, conn}
|
||||
{:error, error} -> {:error, error, conn}
|
||||
end
|
||||
end
|
||||
|
||||
defp head_response(conn, _url, code, headers, opts) do
|
||||
conn
|
||||
|> put_resp_headers(build_resp_headers(headers, opts))
|
||||
|> send_resp(code, "")
|
||||
end
|
||||
|
||||
defp error_or_redirect(conn, url, code, body, opts) do
|
||||
if Keyword.get(opts, :redirect_on_failure, false) do
|
||||
conn
|
||||
|> Phoenix.Controller.redirect(external: url)
|
||||
|> halt()
|
||||
else
|
||||
conn
|
||||
|> send_resp(code, body)
|
||||
|> halt
|
||||
end
|
||||
end
|
||||
|
||||
defp downcase_headers(headers) do
|
||||
Enum.map(headers, fn {k, v} ->
|
||||
{String.downcase(k), v}
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_content_type(headers) do
|
||||
{_, content_type} =
|
||||
List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"})
|
||||
|
||||
[content_type | _] = String.split(content_type, ";")
|
||||
content_type
|
||||
end
|
||||
|
||||
defp put_resp_headers(conn, headers) do
|
||||
Enum.reduce(headers, conn, fn {k, v}, conn ->
|
||||
put_resp_header(conn, k, v)
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_req_headers(headers, opts) do
|
||||
headers
|
||||
|> downcase_headers()
|
||||
|> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
|
||||
|> (fn headers ->
|
||||
headers = headers ++ Keyword.get(opts, :req_headers, [])
|
||||
|
||||
if Keyword.get(opts, :keep_user_agent, false) do
|
||||
List.keystore(
|
||||
headers,
|
||||
"user-agent",
|
||||
0,
|
||||
{"user-agent", Mobilizon.Application.user_agent()}
|
||||
)
|
||||
else
|
||||
headers
|
||||
end
|
||||
end).()
|
||||
end
|
||||
|
||||
defp build_resp_headers(headers, opts) do
|
||||
headers
|
||||
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|
||||
|> build_resp_cache_headers(opts)
|
||||
|> build_resp_content_disposition_header(opts)
|
||||
|> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
|
||||
end
|
||||
|
||||
defp build_resp_cache_headers(headers, _opts) do
|
||||
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
|
||||
has_cache_control? = List.keymember?(headers, "cache-control", 0)
|
||||
|
||||
cond do
|
||||
has_cache? && has_cache_control? ->
|
||||
headers
|
||||
|
||||
has_cache? ->
|
||||
# There's caching header present but no cache-control -- we need to explicitely override it
|
||||
# to public as Plug defaults to "max-age=0, private, must-revalidate"
|
||||
List.keystore(headers, "cache-control", 0, {"cache-control", "public"})
|
||||
|
||||
true ->
|
||||
List.keystore(
|
||||
headers,
|
||||
"cache-control",
|
||||
0,
|
||||
{"cache-control", @default_cache_control_header}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp build_resp_content_disposition_header(headers, opts) do
|
||||
opt = Keyword.get(opts, :inline_content_types, @inline_content_types)
|
||||
|
||||
content_type = get_content_type(headers)
|
||||
|
||||
attachment? =
|
||||
cond do
|
||||
is_list(opt) && !Enum.member?(opt, content_type) -> true
|
||||
opt == false -> true
|
||||
true -> false
|
||||
end
|
||||
|
||||
if attachment? do
|
||||
name =
|
||||
try do
|
||||
{{"content-disposition", content_disposition_string}, _} =
|
||||
List.keytake(headers, "content-disposition", 0)
|
||||
|
||||
[name | _] =
|
||||
Regex.run(
|
||||
~r/filename="((?:[^"\\]|\\.)*)"/u,
|
||||
content_disposition_string || "",
|
||||
capture: :all_but_first
|
||||
)
|
||||
|
||||
name
|
||||
rescue
|
||||
MatchError -> Keyword.get(opts, :attachment_name, "attachment")
|
||||
end
|
||||
|
||||
disposition = "attachment; filename=\"#{name}\""
|
||||
|
||||
List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition})
|
||||
else
|
||||
headers
|
||||
end
|
||||
end
|
||||
|
||||
defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do
|
||||
with {_, size} <- List.keyfind(headers, "content-length", 0),
|
||||
{size, _} <- Integer.parse(size),
|
||||
true <- size <= limit do
|
||||
:ok
|
||||
else
|
||||
false ->
|
||||
{:error, :body_too_large}
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp header_length_constraint(_, _), do: :ok
|
||||
|
||||
defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do
|
||||
{:error, :body_too_large}
|
||||
end
|
||||
|
||||
defp body_size_constraint(_, _), do: :ok
|
||||
|
||||
defp check_read_duration(duration, max)
|
||||
when is_integer(duration) and is_integer(max) and max > 0 do
|
||||
if duration > max do
|
||||
{:error, :read_duration_exceeded}
|
||||
else
|
||||
{:ok, {duration, :erlang.system_time(:millisecond)}}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_read_duration(_, _), do: {:ok, :no_duration_limit, :no_duration_limit}
|
||||
|
||||
defp increase_read_duration({previous_duration, started})
|
||||
when is_integer(previous_duration) and is_integer(started) do
|
||||
duration = :erlang.system_time(:millisecond) - started
|
||||
{:ok, previous_duration + duration}
|
||||
end
|
||||
|
||||
defp increase_read_duration(_) do
|
||||
{:ok, :no_duration_limit, :no_duration_limit}
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,7 @@ defmodule MobilizonWeb.Router do
|
||||
use MobilizonWeb, :router
|
||||
|
||||
pipeline :graphql do
|
||||
plug(:accepts, ["json"])
|
||||
# plug(:accepts, ["json"])
|
||||
plug(MobilizonWeb.AuthPipeline)
|
||||
end
|
||||
|
||||
@@ -102,7 +102,6 @@ defmodule MobilizonWeb.Router do
|
||||
scope "/", MobilizonWeb do
|
||||
pipe_through(:browser)
|
||||
|
||||
forward("/uploads", UploadPlug)
|
||||
get("/*path", PageController, :index)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule MobilizonWeb.Schema do
|
||||
"""
|
||||
use Absinthe.Schema
|
||||
|
||||
alias Mobilizon.{Actors, Events, Users, Addresses}
|
||||
alias Mobilizon.{Actors, Events, Users, Addresses, Media}
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Events.{Event, Comment, Participant}
|
||||
|
||||
@@ -14,6 +14,7 @@ defmodule MobilizonWeb.Schema do
|
||||
import_types(Absinthe.Plug.Types)
|
||||
|
||||
import_types(MobilizonWeb.Schema.UserType)
|
||||
import_types(MobilizonWeb.Schema.PictureType)
|
||||
import_types(MobilizonWeb.Schema.ActorInterface)
|
||||
import_types(MobilizonWeb.Schema.Actors.PersonType)
|
||||
import_types(MobilizonWeb.Schema.Actors.GroupType)
|
||||
@@ -32,12 +33,6 @@ defmodule MobilizonWeb.Schema do
|
||||
field(:user, non_null(:user), description: "The user associated to this session")
|
||||
end
|
||||
|
||||
@desc "A picture"
|
||||
object :picture do
|
||||
field(:url, :string, description: "The URL for this picture")
|
||||
field(:url_thumbnail, :string, description: "The URL for this picture's thumbnail")
|
||||
end
|
||||
|
||||
@desc """
|
||||
Represents a notification for an user
|
||||
"""
|
||||
@@ -91,6 +86,7 @@ defmodule MobilizonWeb.Schema do
|
||||
|> Dataloader.add_source(Users, Users.data())
|
||||
|> Dataloader.add_source(Events, Events.data())
|
||||
|> Dataloader.add_source(Addresses, Addresses.data())
|
||||
|> Dataloader.add_source(Media, Media.data())
|
||||
|
||||
Map.put(ctx, :loader, loader)
|
||||
end
|
||||
@@ -112,6 +108,7 @@ defmodule MobilizonWeb.Schema do
|
||||
import_fields(:tag_queries)
|
||||
import_fields(:address_queries)
|
||||
import_fields(:config_queries)
|
||||
import_fields(:picture_queries)
|
||||
end
|
||||
|
||||
@desc """
|
||||
@@ -126,11 +123,6 @@ defmodule MobilizonWeb.Schema do
|
||||
import_fields(:participant_mutations)
|
||||
import_fields(:member_mutations)
|
||||
import_fields(:feed_token_mutations)
|
||||
|
||||
# @desc "Upload a picture"
|
||||
# field :upload_picture, :picture do
|
||||
# arg(:file, non_null(:upload))
|
||||
# resolve(&Resolvers.Upload.upload_picture/3)
|
||||
# end
|
||||
import_fields(:picture_mutations)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,10 +5,11 @@ defmodule MobilizonWeb.Schema.ActorInterface do
|
||||
use Absinthe.Schema.Notation
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Events
|
||||
alias Mobilizon.{Events}
|
||||
|
||||
import_types(MobilizonWeb.Schema.Actors.FollowerType)
|
||||
import_types(MobilizonWeb.Schema.EventType)
|
||||
# import_types(MobilizonWeb.Schema.PictureType)
|
||||
|
||||
@desc "An ActivityPub actor"
|
||||
interface :actor do
|
||||
@@ -27,8 +28,9 @@ defmodule MobilizonWeb.Schema.ActorInterface do
|
||||
)
|
||||
|
||||
field(:suspended, :boolean, description: "If the actor is suspended")
|
||||
field(:avatar_url, :string, description: "The actor's avatar url")
|
||||
field(:banner_url, :string, description: "The actor's banner url")
|
||||
|
||||
field(:avatar, :picture, description: "The actor's avatar picture")
|
||||
field(:banner, :picture, description: "The actor's banner picture")
|
||||
|
||||
# These one should have a privacy setting
|
||||
field(:following, list_of(:follower), description: "List of followings")
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
|
||||
use Absinthe.Schema.Notation
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
import_types(MobilizonWeb.Schema.Actors.MemberType)
|
||||
alias MobilizonWeb.Resolvers
|
||||
alias MobilizonWeb.Resolvers.{Member, Group}
|
||||
alias Mobilizon.Events
|
||||
|
||||
@desc """
|
||||
@@ -29,8 +29,9 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
|
||||
)
|
||||
|
||||
field(:suspended, :boolean, description: "If the actor is suspended")
|
||||
field(:avatar_url, :string, description: "The actor's avatar url")
|
||||
field(:banner_url, :string, description: "The actor's banner url")
|
||||
|
||||
field(:avatar, :picture, description: "The actor's avatar picture")
|
||||
field(:banner, :picture, description: "The actor's banner picture")
|
||||
|
||||
# These one should have a privacy setting
|
||||
field(:following, list_of(:follower), description: "List of followings")
|
||||
@@ -51,7 +52,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
|
||||
)
|
||||
|
||||
field(:members, non_null(list_of(:member)),
|
||||
resolve: &Resolvers.Member.find_members_for_group/3,
|
||||
resolve: &Member.find_members_for_group/3,
|
||||
description: "List of group members"
|
||||
)
|
||||
end
|
||||
@@ -80,13 +81,13 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
|
||||
field :groups, list_of(:group) do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
resolve(&Resolvers.Group.list_groups/3)
|
||||
resolve(&Group.list_groups/3)
|
||||
end
|
||||
|
||||
@desc "Get a group by it's preferred username"
|
||||
field :group, :group do
|
||||
arg(:preferred_username, non_null(:string))
|
||||
resolve(&Resolvers.Group.find_group/3)
|
||||
resolve(&Group.find_group/3)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -101,7 +102,17 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
|
||||
description: "The actor's username which will be the admin (otherwise user's default one)"
|
||||
)
|
||||
|
||||
resolve(&Resolvers.Group.create_group/3)
|
||||
arg(:avatar, :picture_input,
|
||||
description:
|
||||
"The avatar for the group, either as an object or directly the ID of an existing Picture"
|
||||
)
|
||||
|
||||
arg(:banner, :picture_input,
|
||||
description:
|
||||
"The banner for the group, either as an object or directly the ID of an existing Picture"
|
||||
)
|
||||
|
||||
resolve(&Group.create_group/3)
|
||||
end
|
||||
|
||||
@desc "Delete a group"
|
||||
@@ -109,7 +120,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
|
||||
arg(:group_id, non_null(:integer))
|
||||
arg(:actor_id, non_null(:integer))
|
||||
|
||||
resolve(&Resolvers.Group.delete_group/3)
|
||||
resolve(&Group.delete_group/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
|
||||
use Absinthe.Schema.Notation
|
||||
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
|
||||
alias Mobilizon.Events
|
||||
alias MobilizonWeb.Resolvers
|
||||
alias MobilizonWeb.Resolvers.Person
|
||||
import MobilizonWeb.Schema.Utils
|
||||
|
||||
import_types(MobilizonWeb.Schema.Events.FeedTokenType)
|
||||
@@ -34,8 +34,9 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
|
||||
)
|
||||
|
||||
field(:suspended, :boolean, description: "If the actor is suspended")
|
||||
field(:avatar_url, :string, description: "The actor's avatar url")
|
||||
field(:banner_url, :string, description: "The actor's banner url")
|
||||
|
||||
field(:avatar, :picture, description: "The actor's avatar picture")
|
||||
field(:banner, :picture, description: "The actor's banner picture")
|
||||
|
||||
# These one should have a privacy setting
|
||||
field(:following, list_of(:follower), description: "List of followings")
|
||||
@@ -56,25 +57,25 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
|
||||
|
||||
@desc "The list of events this person goes to"
|
||||
field :going_to_events, list_of(:event) do
|
||||
resolve(&Resolvers.Person.person_going_to_events/3)
|
||||
resolve(&Person.person_going_to_events/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :person_queries do
|
||||
@desc "Get the current actor for the logged-in user"
|
||||
field :logged_person, :person do
|
||||
resolve(&Resolvers.Person.get_current_person/3)
|
||||
resolve(&Person.get_current_person/3)
|
||||
end
|
||||
|
||||
@desc "Get a person by it's preferred username"
|
||||
field :person, :person do
|
||||
arg(:preferred_username, non_null(:string))
|
||||
resolve(&Resolvers.Person.find_person/3)
|
||||
resolve(&Person.find_person/3)
|
||||
end
|
||||
|
||||
@desc "Get the persons for an user"
|
||||
field :identities, list_of(:person) do
|
||||
resolve(&Resolvers.Person.identities/3)
|
||||
resolve(&Person.identities/3)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -87,7 +88,17 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
|
||||
|
||||
arg(:summary, :string, description: "The summary for the new profile", default_value: "")
|
||||
|
||||
resolve(handle_errors(&Resolvers.Person.create_person/3))
|
||||
arg(:avatar, :picture_input,
|
||||
description:
|
||||
"The avatar for the profile, either as an object or directly the ID of an existing Picture"
|
||||
)
|
||||
|
||||
arg(:banner, :picture_input,
|
||||
description:
|
||||
"The banner for the profile, either as an object or directly the ID of an existing Picture"
|
||||
)
|
||||
|
||||
resolve(handle_errors(&Person.create_person/3))
|
||||
end
|
||||
|
||||
@desc "Register a first profile on registration"
|
||||
@@ -99,7 +110,17 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
|
||||
arg(:summary, :string, description: "The summary for the new profile", default_value: "")
|
||||
arg(:email, non_null(:string), description: "The email from the user previously created")
|
||||
|
||||
resolve(handle_errors(&Resolvers.Person.register_person/3))
|
||||
arg(:avatar, :picture_input,
|
||||
description:
|
||||
"The avatar for the profile, either as an object or directly the ID of an existing Picture"
|
||||
)
|
||||
|
||||
arg(:banner, :picture_input,
|
||||
description:
|
||||
"The banner for the profile, either as an object or directly the ID of an existing Picture"
|
||||
)
|
||||
|
||||
resolve(handle_errors(&Person.register_person/3))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
import_types(MobilizonWeb.Schema.AddressType)
|
||||
import_types(MobilizonWeb.Schema.Events.ParticipantType)
|
||||
import_types(MobilizonWeb.Schema.TagType)
|
||||
alias MobilizonWeb.Resolvers
|
||||
alias MobilizonWeb.Resolvers.{Picture, Event, Tag}
|
||||
|
||||
@desc "An event"
|
||||
object :event do
|
||||
@@ -23,10 +23,12 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
field(:ends_on, :datetime, description: "Datetime for when the event ends")
|
||||
field(:status, :event_status, description: "Status of the event")
|
||||
field(:visibility, :event_visibility, description: "The event's visibility")
|
||||
# TODO replace me with picture object
|
||||
field(:thumbnail, :string, description: "A thumbnail picture for the event")
|
||||
# TODO replace me with banner
|
||||
field(:large_image, :string, description: "A large picture for the event")
|
||||
|
||||
field(:picture, :picture,
|
||||
description: "The event's picture",
|
||||
resolve: &Picture.picture/3
|
||||
)
|
||||
|
||||
field(:publish_at, :datetime, description: "When the event was published")
|
||||
|
||||
field(:physical_address, :address,
|
||||
@@ -45,19 +47,19 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
field(:attributed_to, :actor, description: "Who the event is attributed to (often a group)")
|
||||
|
||||
field(:tags, list_of(:tag),
|
||||
resolve: &MobilizonWeb.Resolvers.Tag.list_tags_for_event/3,
|
||||
resolve: &Tag.list_tags_for_event/3,
|
||||
description: "The event's tags"
|
||||
)
|
||||
|
||||
field(:category, :string, description: "The event's category")
|
||||
|
||||
field(:participants, list_of(:participant),
|
||||
resolve: &MobilizonWeb.Resolvers.Event.list_participants_for_event/3,
|
||||
resolve: &Event.list_participants_for_event/3,
|
||||
description: "The event's participants"
|
||||
)
|
||||
|
||||
field(:related_events, list_of(:event),
|
||||
resolve: &MobilizonWeb.Resolvers.Event.list_related_events/3,
|
||||
resolve: &Event.list_related_events/3,
|
||||
description: "Events related to this one"
|
||||
)
|
||||
|
||||
@@ -93,13 +95,13 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
field :events, list_of(:event) do
|
||||
arg(:page, :integer, default_value: 1)
|
||||
arg(:limit, :integer, default_value: 10)
|
||||
resolve(&Resolvers.Event.list_events/3)
|
||||
resolve(&Event.list_events/3)
|
||||
end
|
||||
|
||||
@desc "Get an event by uuid"
|
||||
field :event, :event do
|
||||
arg(:uuid, non_null(:uuid))
|
||||
resolve(&Resolvers.Event.find_event/3)
|
||||
resolve(&Event.find_event/3)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -113,15 +115,20 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
arg(:state, :integer)
|
||||
arg(:status, :integer)
|
||||
arg(:public, :boolean)
|
||||
arg(:thumbnail, :string)
|
||||
arg(:large_image, :string)
|
||||
arg(:visibility, :event_visibility, default_value: :private)
|
||||
|
||||
arg(:picture, :picture_input,
|
||||
description:
|
||||
"The picture for the event, either as an object or directly the ID of an existing Picture"
|
||||
)
|
||||
|
||||
arg(:publish_at, :datetime)
|
||||
arg(:online_address, :string)
|
||||
arg(:phone_address, :string)
|
||||
arg(:organizer_actor_id, non_null(:id))
|
||||
arg(:category, non_null(:string))
|
||||
|
||||
resolve(&Resolvers.Event.create_event/3)
|
||||
resolve(&Event.create_event/3)
|
||||
end
|
||||
|
||||
@desc "Delete an event"
|
||||
@@ -129,7 +136,7 @@ defmodule MobilizonWeb.Schema.EventType do
|
||||
arg(:event_id, non_null(:integer))
|
||||
arg(:actor_id, non_null(:integer))
|
||||
|
||||
resolve(&Resolvers.Event.delete_event/3)
|
||||
resolve(&Event.delete_event/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
48
lib/mobilizon_web/schema/picture.ex
Normal file
48
lib/mobilizon_web/schema/picture.ex
Normal file
@@ -0,0 +1,48 @@
|
||||
defmodule MobilizonWeb.Schema.PictureType do
|
||||
@moduledoc """
|
||||
Schema representation for Pictures
|
||||
"""
|
||||
use Absinthe.Schema.Notation
|
||||
alias MobilizonWeb.Resolvers.Picture
|
||||
|
||||
@desc "A picture"
|
||||
object :picture do
|
||||
field(:id, :id, description: "The picture's ID")
|
||||
field(:alt, :string, description: "The picture's alternative text")
|
||||
field(:name, :string, description: "The picture's name")
|
||||
field(:url, :string, description: "The picture's full URL")
|
||||
end
|
||||
|
||||
@desc "An attached picture or a link to a picture"
|
||||
input_object :picture_input do
|
||||
# Either a full picture object
|
||||
field(:picture, :picture_input_object)
|
||||
# Or directly the ID of an existing picture
|
||||
field(:picture_id, :string)
|
||||
end
|
||||
|
||||
@desc "An attached picture"
|
||||
input_object :picture_input_object do
|
||||
field(:name, non_null(:string))
|
||||
field(:alt, :string)
|
||||
field(:file, non_null(:upload))
|
||||
end
|
||||
|
||||
object :picture_queries do
|
||||
@desc "Get a picture"
|
||||
field :picture, :picture do
|
||||
arg(:id, non_null(:string))
|
||||
resolve(&Picture.picture/3)
|
||||
end
|
||||
end
|
||||
|
||||
object :picture_mutations do
|
||||
@desc "Upload a picture"
|
||||
field :upload_picture, :picture do
|
||||
arg(:name, non_null(:string))
|
||||
arg(:alt, :string)
|
||||
arg(:file, non_null(:upload))
|
||||
resolve(&Picture.upload_picture/3)
|
||||
end
|
||||
end
|
||||
end
|
||||
160
lib/mobilizon_web/upload.ex
Normal file
160
lib/mobilizon_web/upload.ex
Normal file
@@ -0,0 +1,160 @@
|
||||
# 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/upload.ex
|
||||
|
||||
defmodule MobilizonWeb.Upload do
|
||||
@moduledoc """
|
||||
Manage user uploads
|
||||
|
||||
Options:
|
||||
* `:type`: presets for activity type (defaults to Document) and size limits from app configuration
|
||||
* `:description`: upload alternative text
|
||||
* `:base_url`: override base url
|
||||
* `:uploader`: override uploader
|
||||
* `:filters`: override filters
|
||||
* `:size_limit`: override size limit
|
||||
* `:activity_type`: override activity type
|
||||
|
||||
The `%MobilizonWeb.Upload{}` struct: all documented fields are meant to be overwritten in filters:
|
||||
|
||||
* `:id` - the upload id.
|
||||
* `:name` - the upload file name.
|
||||
* `:path` - the upload path: set at first to `id/name` but can be changed. Keep in mind that the path
|
||||
is once created permanent and changing it (especially in uploaders) is probably a bad idea!
|
||||
* `:tempfile` - path to the temporary file. Prefer in-place changes on the file rather than changing the
|
||||
path as the temporary file is also tracked by `Plug.Upload{}` and automatically deleted once the request is over.
|
||||
|
||||
Related behaviors:
|
||||
|
||||
* `MobilizonWeb.Uploaders.Uploader`
|
||||
* `MobilizonWeb.Upload.Filter`
|
||||
|
||||
"""
|
||||
alias Ecto.UUID
|
||||
require Logger
|
||||
|
||||
@type source ::
|
||||
Plug.Upload.t()
|
||||
| (data_uri_string :: String.t())
|
||||
| {:from_local, name :: String.t(), id :: String.t(), path :: String.t()}
|
||||
|
||||
@type option ::
|
||||
{:type, :avatar | :banner | :background}
|
||||
| {:description, String.t()}
|
||||
| {:activity_type, String.t()}
|
||||
| {:size_limit, nil | non_neg_integer()}
|
||||
| {:uploader, module()}
|
||||
| {:filters, [module()]}
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
id: String.t(),
|
||||
name: String.t(),
|
||||
tempfile: String.t(),
|
||||
content_type: String.t(),
|
||||
path: String.t()
|
||||
}
|
||||
defstruct [:id, :name, :tempfile, :content_type, :path]
|
||||
|
||||
@spec store(source, options :: [option()]) :: {:ok, Map.t()} | {:error, any()}
|
||||
def store(upload, opts \\ []) do
|
||||
opts = get_opts(opts)
|
||||
|
||||
with {:ok, upload} <- prepare_upload(upload, opts),
|
||||
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
|
||||
{:ok, upload} <- MobilizonWeb.Upload.Filter.filter(opts.filters, upload),
|
||||
{:ok, url_spec} <- MobilizonWeb.Uploaders.Uploader.put_file(opts.uploader, upload) do
|
||||
{:ok,
|
||||
%{
|
||||
"type" => opts.activity_type,
|
||||
"url" => [
|
||||
%{
|
||||
"type" => "Link",
|
||||
"mediaType" => upload.content_type,
|
||||
"href" => url_from_spec(upload, opts.base_url, url_spec)
|
||||
}
|
||||
],
|
||||
"name" => Map.get(opts, :description) || upload.name
|
||||
}}
|
||||
else
|
||||
{:error, error} ->
|
||||
Logger.error(
|
||||
"#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def char_unescaped?(char) do
|
||||
URI.char_unreserved?(char) or char == ?/
|
||||
end
|
||||
|
||||
defp get_opts(opts) do
|
||||
{size_limit, activity_type} =
|
||||
case Keyword.get(opts, :type) do
|
||||
:banner ->
|
||||
{Mobilizon.CommonConfig.get!([:instance, :banner_upload_limit]), "Image"}
|
||||
|
||||
:avatar ->
|
||||
{Mobilizon.CommonConfig.get!([:instance, :avatar_upload_limit]), "Image"}
|
||||
|
||||
_ ->
|
||||
{Mobilizon.CommonConfig.get!([:instance, :upload_limit]), "Document"}
|
||||
end
|
||||
|
||||
%{
|
||||
activity_type: Keyword.get(opts, :activity_type, activity_type),
|
||||
size_limit: Keyword.get(opts, :size_limit, size_limit),
|
||||
uploader: Keyword.get(opts, :uploader, Mobilizon.CommonConfig.get([__MODULE__, :uploader])),
|
||||
filters: Keyword.get(opts, :filters, Mobilizon.CommonConfig.get([__MODULE__, :filters])),
|
||||
description: Keyword.get(opts, :description),
|
||||
base_url:
|
||||
Keyword.get(
|
||||
opts,
|
||||
:base_url,
|
||||
Mobilizon.CommonConfig.get([__MODULE__, :base_url], MobilizonWeb.Endpoint.url())
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
defp prepare_upload(%Plug.Upload{} = file, opts) do
|
||||
with :ok <- check_file_size(file.path, opts.size_limit),
|
||||
{:ok, content_type, name} <- Mobilizon.MIME.file_mime_type(file.path, file.filename) do
|
||||
{:ok,
|
||||
%__MODULE__{
|
||||
id: UUID.generate(),
|
||||
name: name,
|
||||
tempfile: file.path,
|
||||
content_type: content_type
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_file_size(path, size_limit) when is_integer(size_limit) and size_limit > 0 do
|
||||
with {:ok, %{size: size}} <- File.stat(path),
|
||||
true <- size <= size_limit do
|
||||
:ok
|
||||
else
|
||||
false -> {:error, :file_too_large}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp check_file_size(_, _), do: :ok
|
||||
|
||||
defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
|
||||
path =
|
||||
URI.encode(path, &char_unescaped?/1) <>
|
||||
if Mobilizon.CommonConfig.get([__MODULE__, :link_name], false) do
|
||||
"?name=#{URI.encode(name, &char_unescaped?/1)}"
|
||||
else
|
||||
""
|
||||
end
|
||||
|
||||
[base_url, "media", path]
|
||||
|> Path.join()
|
||||
end
|
||||
|
||||
defp url_from_spec(_upload, _base_url, {:url, url}), do: url
|
||||
end
|
||||
42
lib/mobilizon_web/upload/filter.ex
Normal file
42
lib/mobilizon_web/upload/filter.ex
Normal file
@@ -0,0 +1,42 @@
|
||||
# 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/upload/filter.ex
|
||||
|
||||
defmodule MobilizonWeb.Upload.Filter do
|
||||
@moduledoc """
|
||||
Upload Filter behaviour
|
||||
|
||||
This behaviour allows to run filtering actions just before a file is uploaded. This allows to:
|
||||
|
||||
* morph in place the temporary file
|
||||
* change any field of a `Mobilizon.Upload` struct
|
||||
* cancel/stop the upload
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@callback filter(MobilizonWeb.Upload.t()) ::
|
||||
:ok | {:ok, MobilizonWeb.Upload.t()} | {:error, any()}
|
||||
|
||||
@spec filter([module()], MobilizonWeb.Upload.t()) ::
|
||||
{:ok, MobilizonWeb.Upload.t()} | {:error, any()}
|
||||
|
||||
def filter([], upload) do
|
||||
{:ok, upload}
|
||||
end
|
||||
|
||||
def filter([filter | rest], upload) do
|
||||
case filter.filter(upload) do
|
||||
:ok ->
|
||||
filter(rest, upload)
|
||||
|
||||
{:ok, upload} ->
|
||||
filter(rest, upload)
|
||||
|
||||
error ->
|
||||
Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(error)}")
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
28
lib/mobilizon_web/upload/filter/anonymize_filename.ex
Normal file
28
lib/mobilizon_web/upload/filter/anonymize_filename.ex
Normal file
@@ -0,0 +1,28 @@
|
||||
# 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/upload/filter/anonymize_filename.ex
|
||||
|
||||
defmodule MobilizonWeb.Upload.Filter.AnonymizeFilename do
|
||||
@moduledoc """
|
||||
Replaces the original filename with a pre-defined text or randomly generated string.
|
||||
|
||||
Should be used after `MobilizonWeb.Upload.Filter.Dedupe`.
|
||||
"""
|
||||
@behaviour MobilizonWeb.Upload.Filter
|
||||
|
||||
def filter(upload) do
|
||||
extension = List.last(String.split(upload.name, "."))
|
||||
name = Mobilizon.CommonConfig.get([__MODULE__, :text], random(extension))
|
||||
{:ok, %MobilizonWeb.Upload{upload | name: name}}
|
||||
end
|
||||
|
||||
defp random(extension) do
|
||||
string =
|
||||
10
|
||||
|> :crypto.strong_rand_bytes()
|
||||
|> Base.url_encode64(padding: false)
|
||||
|
||||
string <> "." <> extension
|
||||
end
|
||||
end
|
||||
19
lib/mobilizon_web/upload/filter/dedupe.ex
Normal file
19
lib/mobilizon_web/upload/filter/dedupe.ex
Normal file
@@ -0,0 +1,19 @@
|
||||
# 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/upload/filter/dedupe.ex
|
||||
|
||||
defmodule MobilizonWeb.Upload.Filter.Dedupe do
|
||||
@moduledoc """
|
||||
Names the file after its hash to avoid dedupes
|
||||
"""
|
||||
@behaviour MobilizonWeb.Upload.Filter
|
||||
alias MobilizonWeb.Upload
|
||||
|
||||
def filter(%Upload{name: name} = upload) do
|
||||
extension = String.split(name, ".") |> List.last()
|
||||
shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower)
|
||||
filename = shasum <> "." <> extension
|
||||
{:ok, %Upload{upload | id: shasum, path: filename}}
|
||||
end
|
||||
end
|
||||
45
lib/mobilizon_web/upload/filter/mogrify.ex
Normal file
45
lib/mobilizon_web/upload/filter/mogrify.ex
Normal file
@@ -0,0 +1,45 @@
|
||||
# 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/upload/filter/mogrify.ex
|
||||
|
||||
defmodule MobilizonWeb.Upload.Filter.Mogrify do
|
||||
@moduledoc """
|
||||
Handle mogrify transformations
|
||||
"""
|
||||
@behaviour MobilizonWeb.Upload.Filter
|
||||
|
||||
@type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
|
||||
@type conversions :: conversion() | [conversion()]
|
||||
|
||||
def filter(%MobilizonWeb.Upload{tempfile: file, content_type: "image" <> _}) do
|
||||
filters = Mobilizon.CommonConfig.get!([__MODULE__, :args])
|
||||
|
||||
file
|
||||
|> Mogrify.open()
|
||||
|> mogrify_filter(filters)
|
||||
|> Mogrify.save(in_place: true)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def filter(_), do: :ok
|
||||
|
||||
defp mogrify_filter(mogrify, nil), do: mogrify
|
||||
|
||||
defp mogrify_filter(mogrify, [filter | rest]) do
|
||||
mogrify
|
||||
|> mogrify_filter(filter)
|
||||
|> mogrify_filter(rest)
|
||||
end
|
||||
|
||||
defp mogrify_filter(mogrify, []), do: mogrify
|
||||
|
||||
defp mogrify_filter(mogrify, {action, options}) do
|
||||
Mogrify.custom(mogrify, action, options)
|
||||
end
|
||||
|
||||
defp mogrify_filter(mogrify, action) when is_binary(action) do
|
||||
Mogrify.custom(mogrify, action)
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
defmodule MobilizonWeb.UploadPlug do
|
||||
@moduledoc """
|
||||
Plug to intercept uploads
|
||||
"""
|
||||
use Plug.Builder
|
||||
|
||||
plug(Plug.Static,
|
||||
at: "/",
|
||||
from: {:mobilizon, "./uploads"}
|
||||
)
|
||||
|
||||
# only: ~w(images robots.txt)
|
||||
plug(:not_found)
|
||||
|
||||
def not_found(conn, _) do
|
||||
send_resp(conn, 404, "not found")
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
defmodule MobilizonWeb.Uploaders.Avatar do
|
||||
@moduledoc """
|
||||
Handles avatar uploads
|
||||
"""
|
||||
use Arc.Definition
|
||||
|
||||
# Include ecto support (requires package arc_ecto installed):
|
||||
# use Arc.Ecto.Definition
|
||||
|
||||
@versions [:original]
|
||||
|
||||
# To add a thumbnail version:
|
||||
# @versions [:original, :thumb]
|
||||
|
||||
# Override the bucket on a per definition basis:
|
||||
# def bucket do
|
||||
# :custom_bucket_name
|
||||
# end
|
||||
|
||||
# Whitelist file extensions:
|
||||
# def validate({file, _}) do
|
||||
# ~w(.jpg .jpeg .gif .png) |> Enum.member?(Path.extname(file.file_name))
|
||||
# end
|
||||
|
||||
# Define a thumbnail transformation:
|
||||
# def transform(:thumb, _) do
|
||||
# {:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png", :png}
|
||||
# end
|
||||
|
||||
# Override the persisted filenames:
|
||||
# def filename(version, _) do
|
||||
# version
|
||||
# end
|
||||
|
||||
# Override the storage directory:
|
||||
# def storage_dir(version, {file, scope}) do
|
||||
# "uploads/user/avatars/#{scope.id}"
|
||||
# end
|
||||
|
||||
# Provide a default URL if there hasn't been a file uploaded
|
||||
# def default_url(version, scope) do
|
||||
# "/images/avatars/default_#{version}.png"
|
||||
# end
|
||||
|
||||
# Specify custom headers for s3 objects
|
||||
# Available options are [:cache_control, :content_disposition,
|
||||
# :content_encoding, :content_length, :content_type,
|
||||
# :expect, :expires, :storage_class, :website_redirect_location]
|
||||
#
|
||||
# def s3_object_headers(version, {file, scope}) do
|
||||
# [content_type: MIME.from_path(file.file_name)]
|
||||
# end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
defmodule MobilizonWeb.Uploaders.Category do
|
||||
@moduledoc """
|
||||
Handles file uploads for categories
|
||||
"""
|
||||
use Arc.Definition
|
||||
use Arc.Ecto.Definition
|
||||
|
||||
# To add a thumbnail version:
|
||||
@versions [:original, :thumb]
|
||||
@extension_whitelist ~w(.jpg .jpeg .gif .png)
|
||||
|
||||
# Override the bucket on a per definition basis:
|
||||
# def bucket do
|
||||
# :custom_bucket_name
|
||||
# end
|
||||
|
||||
# Whitelist file extensions:
|
||||
def validate({file, _}) do
|
||||
file_extension = file.file_name |> Path.extname() |> String.downcase()
|
||||
Enum.member?(@extension_whitelist, file_extension)
|
||||
end
|
||||
|
||||
# Define a thumbnail transformation:
|
||||
def transform(:thumb, _) do
|
||||
{:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png", :png}
|
||||
end
|
||||
|
||||
# Override the persisted filenames:
|
||||
def filename(version, {_file, %{title: title}}) do
|
||||
"#{title}_#{version}"
|
||||
end
|
||||
|
||||
# Override the storage directory:
|
||||
def storage_dir(_, _) do
|
||||
"uploads/event/"
|
||||
end
|
||||
|
||||
# Provide a default URL if there hasn't been a file uploaded
|
||||
# def default_url(version, scope) do
|
||||
# "/images/avatars/default_#{version}.png"
|
||||
# end
|
||||
|
||||
# Specify custom headers for s3 objects
|
||||
# Available options are [:cache_control, :content_disposition,
|
||||
# :content_encoding, :content_length, :content_type,
|
||||
# :expect, :expires, :storage_class, :website_redirect_location]
|
||||
#
|
||||
# def s3_object_headers(version, {file, scope}) do
|
||||
# [content_type: MIME.from_path(file.file_name)]
|
||||
# end
|
||||
end
|
||||
40
lib/mobilizon_web/uploaders/local.ex
Normal file
40
lib/mobilizon_web/uploaders/local.ex
Normal file
@@ -0,0 +1,40 @@
|
||||
# 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/uploaders/local.ex
|
||||
|
||||
defmodule MobilizonWeb.Uploaders.Local do
|
||||
@moduledoc """
|
||||
Local uploader for files
|
||||
"""
|
||||
@behaviour MobilizonWeb.Uploaders.Uploader
|
||||
|
||||
def get_file(_) do
|
||||
{:ok, {:static_dir, upload_path()}}
|
||||
end
|
||||
|
||||
def put_file(upload) do
|
||||
{local_path, file} =
|
||||
case Enum.reverse(String.split(upload.path, "/", trim: true)) do
|
||||
[file] ->
|
||||
{upload_path(), file}
|
||||
|
||||
[file | folders] ->
|
||||
path = Path.join([upload_path()] ++ Enum.reverse(folders))
|
||||
File.mkdir_p!(path)
|
||||
{path, file}
|
||||
end
|
||||
|
||||
result_file = Path.join(local_path, file)
|
||||
|
||||
unless File.exists?(result_file) do
|
||||
File.cp!(upload.tempfile, result_file)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def upload_path do
|
||||
Mobilizon.CommonConfig.get!([__MODULE__, :uploads])
|
||||
end
|
||||
end
|
||||
73
lib/mobilizon_web/uploaders/uploader.ex
Normal file
73
lib/mobilizon_web/uploaders/uploader.ex
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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/uploaders/uploader.ex
|
||||
|
||||
defmodule MobilizonWeb.Uploaders.Uploader do
|
||||
@moduledoc """
|
||||
Defines the contract to put and get an uploaded file to any backend.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Instructs how to get the file from the backend.
|
||||
|
||||
Used by `MobilizonWeb.Plugs.UploadedMedia`.
|
||||
"""
|
||||
@type get_method :: {:static_dir, directory :: String.t()} | {:url, url :: String.t()}
|
||||
@callback get_file(file :: String.t()) :: {:ok, get_method()}
|
||||
|
||||
@doc """
|
||||
Put a file to the backend.
|
||||
|
||||
Returns:
|
||||
|
||||
* `:ok` which assumes `{:ok, upload.path}`
|
||||
* `{:ok, spec}` where spec is:
|
||||
* `{:file, filename :: String.t}` to handle reads with `get_file/1` (recommended)
|
||||
|
||||
This allows to correctly proxy or redirect requests to the backend, while allowing to migrate backends without breaking any URL.
|
||||
* `{url, url :: String.t}` to bypass `get_file/2` and use the `url` directly in the activity.
|
||||
* `{:error, String.t}` error information if the file failed to be saved to the backend.
|
||||
* `:wait_callback` will wait for an http post request at `/api/pleroma/upload_callback/:upload_path` and call the uploader's `http_callback/3` method.
|
||||
|
||||
"""
|
||||
@type file_spec :: {:file | :url, String.t()}
|
||||
@callback put_file(MobilizonWeb.Upload.t()) ::
|
||||
:ok | {:ok, file_spec()} | {:error, String.t()} | :wait_callback
|
||||
|
||||
@callback http_callback(Plug.Conn.t(), Map.t()) ::
|
||||
{:ok, Plug.Conn.t()}
|
||||
| {:ok, Plug.Conn.t(), file_spec()}
|
||||
| {:error, Plug.Conn.t(), String.t()}
|
||||
@optional_callbacks http_callback: 2
|
||||
|
||||
@spec put_file(module(), MobilizonWeb.Upload.t()) :: {:ok, file_spec()} | {:error, String.t()}
|
||||
|
||||
def put_file(uploader, upload) do
|
||||
case uploader.put_file(upload) do
|
||||
:ok -> {:ok, {:file, upload.path}}
|
||||
:wait_callback -> handle_callback(uploader, upload)
|
||||
{:ok, _} = ok -> ok
|
||||
{:error, _} = error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_callback(uploader, upload) do
|
||||
:global.register_name({__MODULE__, upload.path}, self())
|
||||
|
||||
receive do
|
||||
{__MODULE__, pid, conn, params} ->
|
||||
case uploader.http_callback(conn, params) do
|
||||
{:ok, conn, ok} ->
|
||||
send(pid, {__MODULE__, conn})
|
||||
{:ok, ok}
|
||||
|
||||
{:error, conn, error} ->
|
||||
send(pid, {__MODULE__, conn})
|
||||
{:error, error}
|
||||
end
|
||||
after
|
||||
30_000 -> {:error, "Uploader callback timeout"}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -8,15 +8,12 @@ defmodule MobilizonWeb.JsonLD.ObjectView do
|
||||
|
||||
def render("event.json", %{event: %Event{} = event}) do
|
||||
# TODO: event.description is actually markdown!
|
||||
|
||||
json_ld = %{
|
||||
"@context" => "https://schema.org",
|
||||
"@type" => "Event",
|
||||
"name" => event.title,
|
||||
"description" => event.description,
|
||||
"image" => [
|
||||
event.thumbnail,
|
||||
event.large_image
|
||||
],
|
||||
"performer" => %{
|
||||
"@type" =>
|
||||
if(event.organizer_actor.type == :Group, do: "PerformingGroup", else: "Person"),
|
||||
@@ -25,6 +22,15 @@ defmodule MobilizonWeb.JsonLD.ObjectView do
|
||||
"location" => render_one(event.physical_address, ObjectView, "place.json", as: :address)
|
||||
}
|
||||
|
||||
json_ld =
|
||||
if event.picture do
|
||||
Map.put(json_ld, "image", [
|
||||
event.picture.file.url
|
||||
])
|
||||
else
|
||||
json_ld
|
||||
end
|
||||
|
||||
json_ld =
|
||||
if event.begins_on,
|
||||
do: Map.put(json_ld, "startDate", DateTime.to_iso8601(event.begins_on)),
|
||||
|
||||
@@ -44,7 +44,7 @@ defmodule MobilizonWeb.PageView do
|
||||
end
|
||||
|
||||
def render("event.activity-json", %{conn: %{assigns: %{object: event}}}) do
|
||||
event = Utils.make_event_data(event)
|
||||
event = Mobilizon.Service.ActivityPub.Converters.Event.model_to_as(event)
|
||||
{:ok, html, []} = Earmark.as_html(event["summary"])
|
||||
|
||||
%{
|
||||
@@ -66,7 +66,7 @@ defmodule MobilizonWeb.PageView do
|
||||
end
|
||||
|
||||
def render("comment.activity-json", %{conn: %{assigns: %{object: comment}}}) do
|
||||
comment = Utils.make_comment_data(comment)
|
||||
comment = Mobilizon.Service.ActivityPub.Converters.Comment.model_to_as(comment)
|
||||
|
||||
%{
|
||||
"actor" => comment["actor"],
|
||||
|
||||
@@ -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
|
||||
|
||||
9
lib/service/activity_pub/converter.ex
Normal file
9
lib/service/activity_pub/converter.ex
Normal 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
|
||||
47
lib/service/activity_pub/converters/actor.ex
Normal file
47
lib/service/activity_pub/converters/actor.ex
Normal 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
|
||||
96
lib/service/activity_pub/converters/comment.ex
Normal file
96
lib/service/activity_pub/converters/comment.ex
Normal 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
|
||||
70
lib/service/activity_pub/converters/event.ex
Normal file
70
lib/service/activity_pub/converters/event.ex
Normal 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
|
||||
@@ -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 """
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user