aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorEgor Kislitsyn <egor@kislitsyn.com>2018-12-06 20:15:16 +0700
committerEgor Kislitsyn <egor@kislitsyn.com>2018-12-06 20:15:16 +0700
commit4944498133d4a945650201226808b1f09d355014 (patch)
tree02e83bac731ca6b391b63d826d8230bdb0fdaed8 /lib
parentbac58b152495c3ebf72e3ad1c3102de075fcc366 (diff)
parentccf0b46dd6a0390a06847b4a1c3eedc8d8e6c916 (diff)
downloadpleroma-4944498133d4a945650201226808b1f09d355014.tar.gz
Merge branch 'develop' into feature/compat/push-subscriptions
# Conflicts: # lib/pleroma/application.ex # lib/pleroma/plugs/oauth_plug.ex
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/make_moderator.ex15
-rw-r--r--lib/mix/tasks/migrate_local_uploads.ex97
-rw-r--r--lib/mix/tasks/sample_psql.eex1
-rw-r--r--lib/mix/tasks/set_admin.ex18
-rw-r--r--lib/mix/tasks/set_locked.ex17
-rw-r--r--lib/pleroma/application.ex47
-rw-r--r--lib/pleroma/config.ex14
-rw-r--r--lib/pleroma/formatter.ex14
-rw-r--r--lib/pleroma/gopher/server.ex35
-rw-r--r--lib/pleroma/html.ex4
-rw-r--r--lib/pleroma/http/connection.ex27
-rw-r--r--lib/pleroma/http/http.ex47
-rw-r--r--lib/pleroma/http/request_builder.ex126
-rw-r--r--lib/pleroma/mime.ex108
-rw-r--r--lib/pleroma/object.ex9
-rw-r--r--lib/pleroma/plugs/oauth_plug.ex71
-rw-r--r--lib/pleroma/plugs/uploaded_media.ex78
-rw-r--r--lib/pleroma/plugs/user_enabled_plug.ex2
-rw-r--r--lib/pleroma/plugs/user_is_admin_plug.ex2
-rw-r--r--lib/pleroma/reverse_proxy.ex343
-rw-r--r--lib/pleroma/upload.ex338
-rw-r--r--lib/pleroma/upload/filter.ex35
-rw-r--r--lib/pleroma/upload/filter/anonymize_filename.ex10
-rw-r--r--lib/pleroma/upload/filter/dedupe.ex10
-rw-r--r--lib/pleroma/upload/filter/mogrifun.ex60
-rw-r--r--lib/pleroma/upload/filter/mogrify.ex37
-rw-r--r--lib/pleroma/uploaders/local.ex55
-rw-r--r--lib/pleroma/uploaders/mdii.ex19
-rw-r--r--lib/pleroma/uploaders/s3.ex56
-rw-r--r--lib/pleroma/uploaders/swift/keystone.ex4
-rw-r--r--lib/pleroma/uploaders/swift/swift.ex6
-rw-r--r--lib/pleroma/uploaders/swift/uploader.ex13
-rw-r--r--lib/pleroma/uploaders/uploader.ex42
-rw-r--r--lib/pleroma/user.ex177
-rw-r--r--lib/pleroma/user/info.ex167
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex25
-rw-r--r--lib/pleroma/web/activity_pub/mrf/simple_policy.ex2
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex6
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex20
-rw-r--r--lib/pleroma/web/admin_api/admin_api_controller.ex27
-rw-r--r--lib/pleroma/web/common_api/common_api.ex9
-rw-r--r--lib/pleroma/web/endpoint.ex2
-rw-r--r--lib/pleroma/web/federator/retry_queue.ex10
-rw-r--r--lib/pleroma/web/http_signatures/http_signatures.ex2
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api_controller.ex170
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex6
-rw-r--r--lib/pleroma/web/media_proxy/controller.ex135
-rw-r--r--lib/pleroma/web/media_proxy/media_proxy.ex20
-rw-r--r--lib/pleroma/web/ostatus/ostatus.ex18
-rw-r--r--lib/pleroma/web/router.ex7
-rw-r--r--lib/pleroma/web/salmon/salmon.ex14
-rw-r--r--lib/pleroma/web/streamer.ex4
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api.ex18
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api_controller.ex191
-rw-r--r--lib/pleroma/web/twitter_api/views/user_view.ex14
-rw-r--r--lib/pleroma/web/web_finger/web_finger.ex24
-rw-r--r--lib/pleroma/web/websub/websub.ex18
57 files changed, 2040 insertions, 806 deletions
diff --git a/lib/mix/tasks/make_moderator.ex b/lib/mix/tasks/make_moderator.ex
index 15586dc30..8dc0a04dd 100644
--- a/lib/mix/tasks/make_moderator.ex
+++ b/lib/mix/tasks/make_moderator.ex
@@ -8,7 +8,7 @@ defmodule Mix.Tasks.SetModerator do
"""
use Mix.Task
- import Mix.Ecto
+ import Ecto.Changeset
alias Pleroma.{Repo, User}
def run([nickname | rest]) do
@@ -21,14 +21,15 @@ defmodule Mix.Tasks.SetModerator do
end
with %User{local: true} = user <- User.get_by_nickname(nickname) do
- info =
- user.info
- |> Map.put("is_moderator", !!moderator)
+ info_cng = User.Info.admin_api_update(user.info, %{is_moderator: !!moderator})
- cng = User.info_changeset(user, %{info: info})
- {:ok, user} = User.update_and_set_cache(cng)
+ user_cng =
+ Ecto.Changeset.change(user)
+ |> put_embed(:info, info_cng)
- IO.puts("Moderator status of #{nickname}: #{user.info["is_moderator"]}")
+ {:ok, user} = User.update_and_set_cache(user_cng)
+
+ IO.puts("Moderator status of #{nickname}: #{user.info.is_moderator}")
else
_ ->
IO.puts("No local user #{nickname}")
diff --git a/lib/mix/tasks/migrate_local_uploads.ex b/lib/mix/tasks/migrate_local_uploads.ex
new file mode 100644
index 000000000..8f9e210c0
--- /dev/null
+++ b/lib/mix/tasks/migrate_local_uploads.ex
@@ -0,0 +1,97 @@
+defmodule Mix.Tasks.MigrateLocalUploads do
+ use Mix.Task
+ import Mix.Ecto
+ alias Pleroma.{Upload, Uploaders.Local, Uploaders.S3}
+ require Logger
+
+ @log_every 50
+ @shortdoc "Migrate uploads from local to remote storage"
+
+ def run([target_uploader | args]) do
+ delete? = Enum.member?(args, "--delete")
+ Application.ensure_all_started(:pleroma)
+
+ local_path = Pleroma.Config.get!([Local, :uploads])
+ uploader = Module.concat(Pleroma.Uploaders, target_uploader)
+
+ unless Code.ensure_loaded?(uploader) do
+ raise("The uploader #{inspect(uploader)} is not an existing/loaded module.")
+ end
+
+ target_enabled? = Pleroma.Config.get([Upload, :uploader]) == uploader
+
+ unless target_enabled? do
+ Pleroma.Config.put([Upload, :uploader], uploader)
+ end
+
+ Logger.info("Migrating files from local #{local_path} to #{to_string(uploader)}")
+
+ if delete? do
+ Logger.warn(
+ "Attention: uploaded files will be deleted, hope you have backups! (--delete ; cancel with ^C)"
+ )
+
+ :timer.sleep(:timer.seconds(5))
+ end
+
+ uploads =
+ File.ls!(local_path)
+ |> Enum.map(fn id ->
+ root_path = Path.join(local_path, id)
+
+ cond do
+ File.dir?(root_path) ->
+ files = for file <- File.ls!(root_path), do: {id, file, Path.join([root_path, file])}
+
+ case List.first(files) do
+ {id, file, path} ->
+ {%Pleroma.Upload{id: id, name: file, path: id <> "/" <> file, tempfile: path},
+ root_path}
+
+ _ ->
+ nil
+ end
+
+ File.exists?(root_path) ->
+ file = Path.basename(id)
+ [hash, ext] = String.split(id, ".")
+ {%Pleroma.Upload{id: hash, name: file, path: file, tempfile: root_path}, root_path}
+
+ true ->
+ nil
+ end
+ end)
+ |> Enum.filter(& &1)
+
+ total_count = length(uploads)
+ Logger.info("Found #{total_count} uploads")
+
+ uploads
+ |> Task.async_stream(
+ fn {upload, root_path} ->
+ case Upload.store(upload, uploader: uploader, filters: [], size_limit: nil) do
+ {:ok, _} ->
+ if delete?, do: File.rm_rf!(root_path)
+ Logger.debug("uploaded: #{inspect(upload.path)} #{inspect(upload)}")
+ :ok
+
+ error ->
+ Logger.error("failed to upload #{inspect(upload.path)}: #{inspect(error)}")
+ end
+ end,
+ timeout: 150_000
+ )
+ |> Stream.chunk_every(@log_every)
+ |> Enum.reduce(0, fn done, count ->
+ count = count + length(done)
+ Logger.info("Uploaded #{count}/#{total_count} files")
+ count
+ end)
+
+ Logger.info("Done!")
+ end
+
+ def run(_) do
+ Logger.error("Usage: migrate_local_uploads S3|Swift [--delete]")
+ end
+end
diff --git a/lib/mix/tasks/sample_psql.eex b/lib/mix/tasks/sample_psql.eex
index b6f57948b..c89b34ef2 100644
--- a/lib/mix/tasks/sample_psql.eex
+++ b/lib/mix/tasks/sample_psql.eex
@@ -4,3 +4,4 @@ CREATE DATABASE pleroma_dev OWNER pleroma;
--Extensions made by ecto.migrate that need superuser access
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
diff --git a/lib/mix/tasks/set_admin.ex b/lib/mix/tasks/set_admin.ex
index d5ccf261b..ac26516f1 100644
--- a/lib/mix/tasks/set_admin.ex
+++ b/lib/mix/tasks/set_admin.ex
@@ -1,5 +1,6 @@
defmodule Mix.Tasks.SetAdmin do
use Mix.Task
+ import Ecto.Changeset
alias Pleroma.User
@doc """
@@ -9,21 +10,22 @@ defmodule Mix.Tasks.SetAdmin do
def run([nickname | rest]) do
Application.ensure_all_started(:pleroma)
- status =
+ admin =
case rest do
- [status] -> status == "true"
+ [admin] -> admin == "true"
_ -> true
end
with %User{local: true} = user <- User.get_by_nickname(nickname) do
- info =
- user.info
- |> Map.put("is_admin", !!status)
+ info_cng = User.Info.admin_api_update(user.info, %{is_admin: !!admin})
- cng = User.info_changeset(user, %{info: info})
- {:ok, user} = User.update_and_set_cache(cng)
+ user_cng =
+ Ecto.Changeset.change(user)
+ |> put_embed(:info, info_cng)
- IO.puts("Admin status of #{nickname}: #{user.info["is_admin"]}")
+ {:ok, user} = User.update_and_set_cache(user_cng)
+
+ IO.puts("Admin status of #{nickname}: #{user.info.is_admin}")
else
_ ->
IO.puts("No local user #{nickname}")
diff --git a/lib/mix/tasks/set_locked.ex b/lib/mix/tasks/set_locked.ex
index a154595ca..e93a63505 100644
--- a/lib/mix/tasks/set_locked.ex
+++ b/lib/mix/tasks/set_locked.ex
@@ -10,11 +10,11 @@ defmodule Mix.Tasks.SetLocked do
"""
use Mix.Task
- import Mix.Ecto
+ import Ecto.Changeset
alias Pleroma.{Repo, User}
def run([nickname | rest]) do
- ensure_started(Repo, [])
+ Application.ensure_all_started(:pleroma)
locked =
case rest do
@@ -23,14 +23,15 @@ defmodule Mix.Tasks.SetLocked do
end
with %User{local: true} = user <- User.get_by_nickname(nickname) do
- info =
- user.info
- |> Map.put("locked", !!locked)
+ info_cng = User.Info.profile_update(user.info, %{locked: !!locked})
- cng = User.info_changeset(user, %{info: info})
- user = Repo.update!(cng)
+ user_cng =
+ Ecto.Changeset.change(user)
+ |> put_embed(:info, info_cng)
- IO.puts("locked status of #{nickname}: #{user.info["locked"]}")
+ {:ok, user} = User.update_and_set_cache(user_cng)
+
+ IO.puts("Locked status of #{nickname}: #{user.info.locked}")
else
_ ->
IO.puts("No local user #{nickname}")
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index d0f23527f..0b0ec0197 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -1,5 +1,6 @@
defmodule Pleroma.Application do
use Application
+ import Supervisor.Spec
@name "Pleroma"
@version Mix.Project.config()[:version]
@@ -7,11 +8,15 @@ defmodule Pleroma.Application do
def version, do: @version
def named_version(), do: @name <> " " <> @version
+ def user_agent() do
+ info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
+ named_version() <> "; " <> info
+ end
+
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
@env Mix.env()
def start(_type, _args) do
- import Supervisor.Spec
import Cachex.Spec
# Define workers and child supervisors to be supervised
@@ -20,10 +25,6 @@ defmodule Pleroma.Application do
# Start the Ecto repository
supervisor(Pleroma.Repo, []),
worker(Pleroma.Emoji, []),
- # Start the endpoint when the application starts
- supervisor(Pleroma.Web.Endpoint, []),
- # Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
- # worker(Pleroma.Worker, [arg1, arg2, arg3]),
worker(
Cachex,
[
@@ -63,21 +64,18 @@ defmodule Pleroma.Application do
],
id: :cachex_idem
),
- worker(Pleroma.Web.Federator, []),
worker(Pleroma.Web.Federator.RetryQueue, []),
- worker(Pleroma.Gopher.Server, []),
+ worker(Pleroma.Web.Federator, []),
worker(Pleroma.Stats, []),
worker(Pleroma.Web.Push, [])
] ++
- if @env == :test,
- do: [],
- else:
- [worker(Pleroma.Web.Streamer, [])] ++
- if(
- !chat_enabled(),
- do: [],
- else: [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
- )
+ streamer_child() ++
+ chat_child() ++
+ [
+ # Start the endpoint when the application starts
+ supervisor(Pleroma.Web.Endpoint, []),
+ worker(Pleroma.Gopher.Server, [])
+ ]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
@@ -85,7 +83,20 @@ defmodule Pleroma.Application do
Supervisor.start_link(children, opts)
end
- defp chat_enabled do
- Application.get_env(:pleroma, :chat, []) |> Keyword.get(:enabled)
+ if Mix.env() == :test do
+ defp streamer_child(), do: []
+ defp chat_child(), do: []
+ else
+ defp streamer_child() do
+ [worker(Pleroma.Web.Streamer, [])]
+ end
+
+ defp chat_child() do
+ if Pleroma.Config.get([:chat, :enabled]) do
+ [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
+ else
+ []
+ end
+ end
end
end
diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex
index 15f771b6e..3876ddf1f 100644
--- a/lib/pleroma/config.ex
+++ b/lib/pleroma/config.ex
@@ -39,4 +39,18 @@ defmodule Pleroma.Config do
def put(key, value) do
Application.put_env(:pleroma, key, value)
end
+
+ def delete([key]), do: delete(key)
+
+ def delete([parent_key | keys]) do
+ {_, parent} =
+ Application.get_env(:pleroma, parent_key)
+ |> get_and_update_in(keys, fn _ -> :pop end)
+
+ Application.put_env(:pleroma, parent_key, parent)
+ end
+
+ def delete(key) do
+ Application.delete_env(:pleroma, key)
+ end
end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 26bb17377..5b03e9aeb 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -114,10 +114,10 @@ defmodule Pleroma.Formatter do
subs =
subs ++
- Enum.map(mentions, fn {match, %User{ap_id: ap_id, info: info}, uuid} ->
+ Enum.map(mentions, fn {match, %User{id: id, ap_id: ap_id, info: info}, uuid} ->
ap_id =
- if is_binary(info["source_data"]["url"]) do
- info["source_data"]["url"]
+ if is_binary(info.source_data["url"]) do
+ info.source_data["url"]
else
ap_id
end
@@ -125,7 +125,7 @@ defmodule Pleroma.Formatter do
short_match = String.split(match, "@") |> tl() |> hd()
{uuid,
- "<span><a class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"}
+ "<span><a data-user='#{id}' class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"}
end)
{subs, uuid_text}
@@ -147,7 +147,11 @@ defmodule Pleroma.Formatter do
subs =
subs ++
Enum.map(tags, fn {tag_text, tag, uuid} ->
- url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag_text}</a>"
+ url =
+ "<a data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{
+ tag_text
+ }</a>"
+
{uuid, url}
end)
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
index e6361a82c..3b0569a99 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -6,28 +6,29 @@ defmodule Pleroma.Gopher.Server do
config = Pleroma.Config.get(:gopher, [])
ip = Keyword.get(config, :ip, {0, 0, 0, 0})
port = Keyword.get(config, :port, 1234)
- GenServer.start_link(__MODULE__, [ip, port], [])
- end
- def init([ip, port]) do
- if Pleroma.Config.get([:gopher, :enabled], false) do
- Logger.info("Starting gopher server on #{port}")
-
- :ranch.start_listener(
- :gopher,
- 100,
- :ranch_tcp,
- [port: port],
- __MODULE__.ProtocolHandler,
- []
- )
-
- {:ok, %{ip: ip, port: port}}
+ if Keyword.get(config, :enabled, false) do
+ GenServer.start_link(__MODULE__, [ip, port], [])
else
Logger.info("Gopher server disabled")
- {:ok, nil}
+ :ignore
end
end
+
+ def init([ip, port]) do
+ Logger.info("Starting gopher server on #{port}")
+
+ :ranch.start_listener(
+ :gopher,
+ 100,
+ :ranch_tcp,
+ [port: port],
+ __MODULE__.ProtocolHandler,
+ []
+ )
+
+ {:ok, %{ip: ip, port: port}}
+ end
end
defmodule Pleroma.Gopher.Server.ProtocolHandler do
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 1b920d7fd..5daaa5e69 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -45,7 +45,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.strip_comments()
# links
- Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
+ Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
# paragraphs and linebreaks
@@ -86,7 +86,7 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
- Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
+ Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
Meta.allow_tag_with_these_attributes("abbr", ["title"])
diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex
new file mode 100644
index 000000000..5e8f2aabd
--- /dev/null
+++ b/lib/pleroma/http/connection.ex
@@ -0,0 +1,27 @@
+defmodule Pleroma.HTTP.Connection do
+ @moduledoc """
+ Connection for http-requests.
+ """
+
+ @hackney_options [pool: :default]
+ @adapter Application.get_env(:tesla, :adapter)
+
+ @doc """
+ Configure a client connection
+
+ # Returns
+
+ Tesla.Env.client
+ """
+ @spec new(Keyword.t()) :: Tesla.Env.client()
+ def new(opts \\ []) do
+ Tesla.client([], {@adapter, hackney_options(opts)})
+ end
+
+ # fetch Hackney options
+ #
+ defp hackney_options(opts \\ []) do
+ options = Keyword.get(opts, :adapter, [])
+ @hackney_options ++ options
+ end
+end
diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex
index e64266ae7..3c0256575 100644
--- a/lib/pleroma/http/http.ex
+++ b/lib/pleroma/http/http.ex
@@ -1,14 +1,42 @@
defmodule Pleroma.HTTP do
- require HTTPoison
+ @moduledoc """
+ """
+
+ alias Pleroma.HTTP.Connection
+ alias Pleroma.HTTP.RequestBuilder, as: Builder
+
+ @doc """
+ Builds and perform http request.
+
+ # Arguments:
+ `method` - :get, :post, :put, :delete
+ `url`
+ `body`
+ `headers` - a keyworld list of headers, e.g. `[{"content-type", "text/plain"}]`
+ `options` - custom, per-request middleware or adapter options
+
+ # Returns:
+ `{:ok, %Tesla.Env{}}` or `{:error, error}`
+
+ """
def request(method, url, body \\ "", headers \\ [], options \\ []) do
options =
process_request_options(options)
|> process_sni_options(url)
- HTTPoison.request(method, url, body, headers, options)
+ %{}
+ |> Builder.method(method)
+ |> Builder.headers(headers)
+ |> Builder.opts(options)
+ |> Builder.url(url)
+ |> Builder.add_param(:body, :body, body)
+ |> Enum.into([])
+ |> (&Tesla.request(Connection.new(), &1)).()
end
+ defp process_sni_options(options, nil), do: options
+
defp process_sni_options(options, url) do
uri = URI.parse(url)
host = uri.host |> to_charlist()
@@ -22,7 +50,7 @@ defmodule Pleroma.HTTP do
def process_request_options(options) do
config = Application.get_env(:pleroma, :http, [])
proxy = Keyword.get(config, :proxy_url, nil)
- options = options ++ [hackney: [pool: :default]]
+ options = options ++ [adapter: [pool: :default]]
case proxy do
nil -> options
@@ -30,8 +58,19 @@ defmodule Pleroma.HTTP do
end
end
- def get(url, headers \\ [], options \\ []), do: request(:get, url, "", headers, options)
+ @doc """
+ Performs GET request.
+
+ See `Pleroma.HTTP.request/5`
+ """
+ def get(url, headers \\ [], options \\ []),
+ do: request(:get, url, "", headers, options)
+
+ @doc """
+ Performs POST request.
+ See `Pleroma.HTTP.request/5`
+ """
def post(url, body, headers \\ [], options \\ []),
do: request(:post, url, body, headers, options)
end
diff --git a/lib/pleroma/http/request_builder.ex b/lib/pleroma/http/request_builder.ex
new file mode 100644
index 000000000..5aee2b8ae
--- /dev/null
+++ b/lib/pleroma/http/request_builder.ex
@@ -0,0 +1,126 @@
+defmodule Pleroma.HTTP.RequestBuilder do
+ @moduledoc """
+ Helper functions for building Tesla requests
+ """
+
+ @doc """
+ Specify the request method when building a request
+
+ ## Parameters
+
+ - request (Map) - Collected request options
+ - m (atom) - Request method
+
+ ## Returns
+
+ Map
+ """
+ @spec method(map(), atom) :: map()
+ def method(request, m) do
+ Map.put_new(request, :method, m)
+ end
+
+ @doc """
+ Specify the request method when building a request
+
+ ## Parameters
+
+ - request (Map) - Collected request options
+ - u (String) - Request URL
+
+ ## Returns
+
+ Map
+ """
+ @spec url(map(), String.t()) :: map()
+ def url(request, u) do
+ Map.put_new(request, :url, u)
+ end
+
+ @doc """
+ Add headers to the request
+ """
+ @spec headers(map(), list(tuple)) :: map()
+ def headers(request, h) do
+ Map.put_new(request, :headers, h)
+ end
+
+ @doc """
+ Add custom, per-request middleware or adapter options to the request
+ """
+ @spec opts(map(), Keyword.t()) :: map()
+ def opts(request, options) do
+ Map.put_new(request, :opts, options)
+ end
+
+ @doc """
+ Add optional parameters to the request
+
+ ## Parameters
+
+ - request (Map) - Collected request options
+ - definitions (Map) - Map of parameter name to parameter location.
+ - options (KeywordList) - The provided optional parameters
+
+ ## Returns
+
+ Map
+ """
+ @spec add_optional_params(map(), %{optional(atom) => atom}, keyword()) :: map()
+ def add_optional_params(request, _, []), do: request
+
+ def add_optional_params(request, definitions, [{key, value} | tail]) do
+ case definitions do
+ %{^key => location} ->
+ request
+ |> add_param(location, key, value)
+ |> add_optional_params(definitions, tail)
+
+ _ ->
+ add_optional_params(request, definitions, tail)
+ end
+ end
+
+ @doc """
+ Add optional parameters to the request
+
+ ## Parameters
+
+ - request (Map) - Collected request options
+ - location (atom) - Where to put the parameter
+ - key (atom) - The name of the parameter
+ - value (any) - The value of the parameter
+
+ ## Returns
+
+ Map
+ """
+ @spec add_param(map(), atom, atom, any()) :: map()
+ def add_param(request, :body, :body, value), do: Map.put(request, :body, value)
+
+ def add_param(request, :body, key, value) do
+ request
+ |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
+ |> Map.update!(
+ :body,
+ &Tesla.Multipart.add_field(&1, key, Poison.encode!(value),
+ headers: [{:"Content-Type", "application/json"}]
+ )
+ )
+ end
+
+ def add_param(request, :file, name, path) do
+ request
+ |> Map.put_new_lazy(:body, &Tesla.Multipart.new/0)
+ |> Map.update!(:body, &Tesla.Multipart.add_file(&1, path, name: name))
+ end
+
+ def add_param(request, :form, name, value) do
+ request
+ |> Map.update(:body, %{name => value}, &Map.put(&1, name, value))
+ end
+
+ def add_param(request, location, key, value) do
+ Map.update(request, location, [{key, value}], &(&1 ++ [{key, value}]))
+ end
+end
diff --git a/lib/pleroma/mime.ex b/lib/pleroma/mime.ex
new file mode 100644
index 000000000..db8b7c742
--- /dev/null
+++ b/lib/pleroma/mime.ex
@@ -0,0 +1,108 @@
+defmodule Pleroma.MIME do
+ @moduledoc """
+ Returns the mime-type of a binary and optionally a normalized file-name.
+ """
+ @default "application/octet-stream"
+ @read_bytes 31
+
+ @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 mime_type(<<_::binary>>), do: {:ok, @default}
+
+ def bin_mime_type(_), do: :error
+
+ 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(<<0x52, 0x49, 0x46, 0x46, _::binary>>) do
+ "audio/wav"
+ end
+
+ defp check_mime_type(_) do
+ @default
+ end
+end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 03a75dfbd..31c8dd5bd 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -1,6 +1,6 @@
defmodule Pleroma.Object do
use Ecto.Schema
- alias Pleroma.{Repo, Object, Activity}
+ alias Pleroma.{Repo, Object, User, Activity}
import Ecto.{Query, Changeset}
schema "objects" do
@@ -31,6 +31,13 @@ defmodule Pleroma.Object do
def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
def normalize(_), do: nil
+ # Owned objects can only be mutated by their owner
+ def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
+ do: actor == ap_id
+
+ # Legacy objects can be mutated by anybody
+ def authorize_mutation(%Object{}, %User{}), do: true
+
if Mix.env() == :test do
def get_cached_by_ap_id(ap_id) do
get_by_ap_id(ap_id)
diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex
index 651485e09..8b99a74d1 100644
--- a/lib/pleroma/plugs/oauth_plug.ex
+++ b/lib/pleroma/plugs/oauth_plug.ex
@@ -1,26 +1,22 @@
defmodule Pleroma.Plugs.OAuthPlug do
import Plug.Conn
- alias Pleroma.User
- alias Pleroma.Repo
- alias Pleroma.Web.OAuth.Token
+ import Ecto.Query
- def init(options) do
- options
- end
+ alias Pleroma.{
+ User,
+ Repo,
+ Web.OAuth.Token
+ }
+
+ @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
+
+ def init(options), do: options
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(conn, _) do
- token =
- case get_req_header(conn, "authorization") do
- ["Bearer " <> header] -> header
- _ -> get_session(conn, :oauth_token)
- end
-
- with token when not is_nil(token) <- token,
- %Token{user_id: user_id} = token <- Repo.get_by(Token, token: token),
- %User{} = user <- Repo.get(User, user_id),
- false <- !!user.info["deactivated"] do
+ with {:ok, token} <- fetch_token(conn),
+ {:ok, user} <- fetch_user(token) do
conn
|> assign(:token, token)
|> assign(:user, user)
@@ -28,4 +24,47 @@ defmodule Pleroma.Plugs.OAuthPlug do
_ -> conn
end
end
+
+ # Gets user by token
+ #
+ @spec fetch_user(String.t()) :: {:ok, User.t()} | nil
+ defp fetch_user(token) do
+ query = from(q in Token, where: q.token == ^token, preload: [:user])
+
+ with %Token{user: %{info: %{deactivated: false} = _} = user} <- Repo.one(query) do
+ {:ok, user}
+ end
+ end
+
+ # Gets token from session by :oauth_token key
+ #
+ @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token_from_session(conn) do
+ case get_session(conn, :oauth_token) do
+ nil -> :no_token_found
+ token -> {:ok, token}
+ end
+ end
+
+ # Gets token from headers
+ #
+ @spec fetch_token(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token(%Plug.Conn{} = conn) do
+ headers = get_req_header(conn, "authorization")
+
+ with :no_token_found <- fetch_token(headers),
+ do: fetch_token_from_session(conn)
+ end
+
+ @spec fetch_token(Keyword.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token([]), do: :no_token_found
+
+ defp fetch_token([token | tail]) do
+ trimmed_token = String.trim(token)
+
+ case Regex.run(@realm_reg, trimmed_token) do
+ [_, match] -> {:ok, String.trim(match)}
+ _ -> fetch_token(tail)
+ end
+ end
end
diff --git a/lib/pleroma/plugs/uploaded_media.ex b/lib/pleroma/plugs/uploaded_media.ex
new file mode 100644
index 000000000..994cc8bf6
--- /dev/null
+++ b/lib/pleroma/plugs/uploaded_media.ex
@@ -0,0 +1,78 @@
+defmodule Pleroma.Plugs.UploadedMedia do
+ @moduledoc """
+ """
+
+ import Plug.Conn
+ require Logger
+
+ @behaviour Plug
+ # no slashes
+ @path "media"
+ @cache_control %{
+ default: "public, max-age=1209600",
+ error: "public, must-revalidate, max-age=160"
+ }
+
+ 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(conn = %{request_path: <<"/", @path, "/", file::binary>>}, opts) do
+ config = Pleroma.Config.get([Pleroma.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
+ |> Pleroma.ReverseProxy.call(url, Pleroma.Config.get([Pleroma.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
diff --git a/lib/pleroma/plugs/user_enabled_plug.ex b/lib/pleroma/plugs/user_enabled_plug.ex
index 9c3285896..01482f47d 100644
--- a/lib/pleroma/plugs/user_enabled_plug.ex
+++ b/lib/pleroma/plugs/user_enabled_plug.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Plugs.UserEnabledPlug do
options
end
- def call(%{assigns: %{user: %User{info: %{"deactivated" => true}}}} = conn, _) do
+ def call(%{assigns: %{user: %User{info: %{deactivated: true}}}} = conn, _) do
conn
|> assign(:user, nil)
end
diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex
index 5312f1499..cf22ce5d0 100644
--- a/lib/pleroma/plugs/user_is_admin_plug.ex
+++ b/lib/pleroma/plugs/user_is_admin_plug.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Plugs.UserIsAdminPlug do
options
end
- def call(%{assigns: %{user: %User{info: %{"is_admin" => true}}}} = conn, _) do
+ def call(%{assigns: %{user: %User{info: %{is_admin: true}}}} = conn, _) do
conn
end
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
new file mode 100644
index 000000000..ad9dc82fe
--- /dev/null
+++ b/lib/pleroma/reverse_proxy.ex
@@ -0,0 +1,343 @@
+defmodule Pleroma.ReverseProxy do
+ @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since 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 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.
+
+ Pleroma.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(:pleroma, :hackney, :hackney)
+ @httpoison Application.get_env(:pleroma, :httpoison, HTTPoison)
+
+ @default_hackney_options [{:follow_redirect, true}]
+
+ @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 = %{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 = Pleroma.Web.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 =
+ 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", Pleroma.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)
+
+ if has_cache? do
+ headers
+ else
+ 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
+ disposition = "attachment; filename=" <> Keyword.get(opts, :attachment_name, "attachment")
+ 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
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index 238630bf3..bf2c60102 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -1,190 +1,222 @@
defmodule Pleroma.Upload do
- alias Ecto.UUID
+ @moduledoc """
+ # Upload
- def check_file_size(path, nil), do: true
+ 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
- def check_file_size(path, size_limit) do
- {:ok, %{size: size}} = File.stat(path)
- size <= size_limit
- end
+ The `%Pleroma.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:
- def store(file, should_dedupe, size_limit \\ nil)
-
- def store(%Plug.Upload{} = file, should_dedupe, size_limit) do
- content_type = get_content_type(file.path)
-
- with uuid <- get_uuid(file, should_dedupe),
- name <- get_name(file, uuid, content_type, should_dedupe),
- true <- check_file_size(file.path, size_limit) do
- strip_exif_data(content_type, file.path)
-
- {:ok, url_path} = uploader().put_file(name, uuid, file.path, content_type, should_dedupe)
-
- %{
- "type" => "Document",
- "url" => [
- %{
- "type" => "Link",
- "mediaType" => content_type,
- "href" => url_path
- }
- ],
- "name" => name
- }
+ * `Pleroma.Uploaders.Uploader`
+ * `Pleroma.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} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
+ {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do
+ {:ok,
+ %{
+ "type" => opts.activity_type,
+ "url" => [
+ %{
+ "type" => "Link",
+ "mediaType" => upload.content_type,
+ "href" => url_from_spec(opts.base_url, url_spec)
+ }
+ ],
+ "name" => Map.get(opts, :description) || upload.name
+ }}
else
- _e -> nil
+ {:error, error} ->
+ Logger.error(
+ "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}"
+ )
+
+ {:error, error}
end
end
- def store(%{"img" => "data:image/" <> image_data}, should_dedupe, size_limit) do
- parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
- data = Base.decode64!(parsed["data"], ignore: :whitespace)
+ defp get_opts(opts) do
+ {size_limit, activity_type} =
+ case Keyword.get(opts, :type) do
+ :banner ->
+ {Pleroma.Config.get!([:instance, :banner_upload_limit]), "Image"}
+
+ :avatar ->
+ {Pleroma.Config.get!([:instance, :avatar_upload_limit]), "Image"}
+
+ :background ->
+ {Pleroma.Config.get!([:instance, :background_upload_limit]), "Image"}
- with tmp_path <- tempfile_for_image(data),
- uuid <- UUID.generate(),
- true <- check_file_size(tmp_path, size_limit) do
- content_type = get_content_type(tmp_path)
- strip_exif_data(content_type, tmp_path)
-
- name =
- create_name(
- String.downcase(Base.encode16(:crypto.hash(:sha256, data))),
- parsed["filetype"],
- content_type
+ _ ->
+ {Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
+ end
+
+ opts = %{
+ activity_type: Keyword.get(opts, :activity_type, activity_type),
+ size_limit: Keyword.get(opts, :size_limit, size_limit),
+ uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
+ filters: Keyword.get(opts, :filters, Pleroma.Config.get([__MODULE__, :filters])),
+ description: Keyword.get(opts, :description),
+ base_url:
+ Keyword.get(
+ opts,
+ :base_url,
+ Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
)
+ }
- {:ok, url_path} = uploader().put_file(name, uuid, tmp_path, content_type, should_dedupe)
-
- %{
- "type" => "Image",
- "url" => [
- %{
- "type" => "Link",
- "mediaType" => content_type,
- "href" => url_path
- }
- ],
- "name" => name
- }
- else
- _e -> nil
- end
- end
+ # TODO: 1.0+ : remove old config compatibility
+ opts =
+ if Pleroma.Config.get([__MODULE__, :strip_exif]) == true &&
+ !Enum.member?(opts.filters, Pleroma.Upload.Filter.Mogrify) do
+ Logger.warn("""
+ Pleroma: configuration `:instance, :strip_exif` is deprecated, please instead set:
- @doc """
- Creates a tempfile using the Plug.Upload Genserver which cleans them up
- automatically.
- """
- def tempfile_for_image(data) do
- {:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
- {:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
- IO.binwrite(tmp_file, data)
+ :pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Mogrify]]
- tmp_path
- end
+ :pleroma, Pleroma.Upload.Filter.Mogrify, args: "strip"
+ """)
- def strip_exif_data(content_type, file) do
- settings = Application.get_env(:pleroma, Pleroma.Upload)
- do_strip = Keyword.fetch!(settings, :strip_exif)
- [filetype, _ext] = String.split(content_type, "/")
+ Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: "strip")
+ Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Mogrify])
+ else
+ opts
+ end
- if filetype == "image" and do_strip == true do
- Mogrify.open(file) |> Mogrify.custom("strip") |> Mogrify.save(in_place: true)
- end
- end
+ opts =
+ if Pleroma.Config.get([:instance, :dedupe_media]) == true &&
+ !Enum.member?(opts.filters, Pleroma.Upload.Filter.Dedupe) do
+ Logger.warn("""
+ Pleroma: configuration `:instance, :dedupe_media` is deprecated, please instead set:
- defp create_name(uuid, ext, type) do
- case type do
- "application/octet-stream" ->
- String.downcase(Enum.join([uuid, ext], "."))
+ :pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Dedupe]]
+ """)
- "audio/mpeg" ->
- String.downcase(Enum.join([uuid, "mp3"], "."))
+ Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Dedupe])
+ else
+ opts
+ end
+ end
- _ ->
- String.downcase(Enum.join([uuid, List.last(String.split(type, "/"))], "."))
+ defp prepare_upload(%Plug.Upload{} = file, opts) do
+ with :ok <- check_file_size(file.path, opts.size_limit),
+ {:ok, content_type, name} <- Pleroma.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 get_uuid(file, should_dedupe) do
- if should_dedupe do
- Base.encode16(:crypto.hash(:sha256, File.read!(file.path)))
- else
- UUID.generate()
+ defp prepare_upload(%{"img" => "data:image/" <> image_data}, opts) do
+ parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
+ data = Base.decode64!(parsed["data"], ignore: :whitespace)
+ hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))
+
+ with :ok <- check_binary_size(data, opts.size_limit),
+ tmp_path <- tempfile_for_image(data),
+ {:ok, content_type, name} <-
+ Pleroma.MIME.bin_mime_type(data, hash <> "." <> parsed["filetype"]) do
+ {:ok,
+ %__MODULE__{
+ id: UUID.generate(),
+ name: name,
+ tempfile: tmp_path,
+ content_type: content_type
+ }}
end
end
- defp get_name(file, uuid, type, should_dedupe) do
- if should_dedupe do
- create_name(uuid, List.last(String.split(file.filename, ".")), type)
- else
- parts = String.split(file.filename, ".")
-
- new_filename =
- if length(parts) > 1 do
- Enum.drop(parts, -1) |> Enum.join(".")
- else
- Enum.join(parts)
- end
-
- case type do
- "application/octet-stream" -> file.filename
- "audio/mpeg" -> new_filename <> ".mp3"
- "image/jpeg" -> new_filename <> ".jpg"
- _ -> Enum.join([new_filename, String.split(type, "/") |> List.last()], ".")
- end
+ # For Mix.Tasks.MigrateLocalUploads
+ defp prepare_upload(upload = %__MODULE__{tempfile: path}, _opts) do
+ with {:ok, content_type} <- Pleroma.MIME.file_mime_type(path) do
+ {:ok, %__MODULE__{upload | content_type: content_type}}
end
end
- def get_content_type(file) do
- match =
- File.open(file, [:read], fn f ->
- case IO.binread(f, 8) do
- <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> ->
- "image/png"
-
- <<0x47, 0x49, 0x46, 0x38, _, 0x61, _, _>> ->
- "image/gif"
-
- <<0xFF, 0xD8, 0xFF, _, _, _, _, _>> ->
- "image/jpeg"
-
- <<0x1A, 0x45, 0xDF, 0xA3, _, _, _, _>> ->
- "video/webm"
-
- <<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70>> ->
- "video/mp4"
-
- <<0x49, 0x44, 0x33, _, _, _, _, _>> ->
- "audio/mpeg"
+ defp check_binary_size(binary, size_limit)
+ when is_integer(size_limit) and size_limit > 0 and byte_size(binary) >= size_limit do
+ {:error, :file_too_large}
+ end
- <<255, 251, _, 68, 0, 0, 0, 0>> ->
- "audio/mpeg"
+ defp check_binary_size(_, _), do: :ok
- <<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
- case IO.binread(f, 27) do
- <<_::size(160), 0x80, 0x74, 0x68, 0x65, 0x6F, 0x72, 0x61>> ->
- "video/ogg"
+ 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
- _ ->
- "audio/ogg"
- end
+ defp check_file_size(_, _), do: :ok
- <<0x52, 0x49, 0x46, 0x46, _, _, _, _>> ->
- "audio/wav"
+ # Creates a tempfile using the Plug.Upload Genserver which cleans them up
+ # automatically.
+ defp tempfile_for_image(data) do
+ {:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
+ {:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
+ IO.binwrite(tmp_file, data)
- _ ->
- "application/octet-stream"
- end
- end)
+ tmp_path
+ end
- case match do
- {:ok, type} -> type
- _e -> "application/octet-stream"
- end
+ defp url_from_spec(base_url, {:file, path}) do
+ [base_url, "media", path]
+ |> Path.join()
end
- defp uploader() do
- Pleroma.Config.get!([Pleroma.Upload, :uploader])
+ defp url_from_spec({:url, url}) do
+ url
end
end
diff --git a/lib/pleroma/upload/filter.ex b/lib/pleroma/upload/filter.ex
new file mode 100644
index 000000000..d1384ddad
--- /dev/null
+++ b/lib/pleroma/upload/filter.ex
@@ -0,0 +1,35 @@
+defmodule Pleroma.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 `Pleroma.Upload` struct
+ * cancel/stop the upload
+ """
+
+ require Logger
+
+ @callback filter(Pleroma.Upload.t()) :: :ok | {:ok, Pleroma.Upload.t()} | {:error, any()}
+
+ @spec filter([module()], Pleroma.Upload.t()) :: {:ok, Pleroma.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
diff --git a/lib/pleroma/upload/filter/anonymize_filename.ex b/lib/pleroma/upload/filter/anonymize_filename.ex
new file mode 100644
index 000000000..a83e764e5
--- /dev/null
+++ b/lib/pleroma/upload/filter/anonymize_filename.ex
@@ -0,0 +1,10 @@
+defmodule Pleroma.Upload.Filter.AnonymizeFilename do
+ @moduledoc "Replaces the original filename with a randomly generated string."
+ @behaviour Pleroma.Upload.Filter
+
+ def filter(upload) do
+ extension = List.last(String.split(upload.name, "."))
+ string = Base.url_encode64(:crypto.strong_rand_bytes(10), padding: false)
+ {:ok, %Pleroma.Upload{upload | name: string <> "." <> extension}}
+ end
+end
diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex
new file mode 100644
index 000000000..28091a627
--- /dev/null
+++ b/lib/pleroma/upload/filter/dedupe.ex
@@ -0,0 +1,10 @@
+defmodule Pleroma.Upload.Filter.Dedupe do
+ @behaviour Pleroma.Upload.Filter
+
+ def filter(upload = %Pleroma.Upload{name: name, tempfile: path}) do
+ extension = String.split(name, ".") |> List.last()
+ shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower)
+ filename = shasum <> "." <> extension
+ {:ok, %Pleroma.Upload{upload | id: shasum, path: filename}}
+ end
+end
diff --git a/lib/pleroma/upload/filter/mogrifun.ex b/lib/pleroma/upload/filter/mogrifun.ex
new file mode 100644
index 000000000..4d4f0b401
--- /dev/null
+++ b/lib/pleroma/upload/filter/mogrifun.ex
@@ -0,0 +1,60 @@
+defmodule Pleroma.Upload.Filter.Mogrifun do
+ @behaviour Pleroma.Upload.Filter
+
+ @filters [
+ {"implode", "1"},
+ {"-raise", "20"},
+ {"+raise", "20"},
+ [{"-interpolate", "nearest"}, {"-virtual-pixel", "mirror"}, {"-spread", "5"}],
+ "+polaroid",
+ {"-statistic", "Mode 10"},
+ {"-emboss", "0x1.1"},
+ {"-emboss", "0x2"},
+ {"-colorspace", "Gray"},
+ "-negate",
+ [{"-channel", "green"}, "-negate"],
+ [{"-channel", "red"}, "-negate"],
+ [{"-channel", "blue"}, "-negate"],
+ {"+level-colors", "green,gold"},
+ {"+level-colors", ",DodgerBlue"},
+ {"+level-colors", ",Gold"},
+ {"+level-colors", ",Lime"},
+ {"+level-colors", ",Red"},
+ {"+level-colors", ",DarkGreen"},
+ {"+level-colors", "firebrick,yellow"},
+ {"+level-colors", "'rgb(102,75,25)',lemonchiffon"},
+ [{"fill", "red"}, {"tint", "40"}],
+ [{"fill", "green"}, {"tint", "40"}],
+ [{"fill", "blue"}, {"tint", "40"}],
+ [{"fill", "yellow"}, {"tint", "40"}]
+ ]
+
+ def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
+ filter = Enum.random(@filters)
+
+ file
+ |> Mogrify.open()
+ |> mogrify_filter(filter)
+ |> Mogrify.save(in_place: true)
+
+ :ok
+ end
+
+ def filter(_), do: :ok
+
+ 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, string) when is_binary(string) do
+ Mogrify.custom(mogrify, string)
+ end
+end
diff --git a/lib/pleroma/upload/filter/mogrify.ex b/lib/pleroma/upload/filter/mogrify.ex
new file mode 100644
index 000000000..d6ed471ed
--- /dev/null
+++ b/lib/pleroma/upload/filter/mogrify.ex
@@ -0,0 +1,37 @@
+defmodule Pleroma.Upload.Filter.Mogrify do
+ @behaviour Pleroma.Uploader.Filter
+
+ @type conversion :: action :: String.t() | {action :: String.t(), opts :: String.t()}
+ @type conversions :: conversion() | [conversion()]
+
+ def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
+ filters = Pleroma.Config.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
diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex
index d96481c8d..434a6b515 100644
--- a/lib/pleroma/uploaders/local.ex
+++ b/lib/pleroma/uploaders/local.ex
@@ -3,49 +3,32 @@ defmodule Pleroma.Uploaders.Local do
alias Pleroma.Web
- def put_file(name, uuid, tmpfile, _content_type, should_dedupe) do
- upload_folder = get_upload_path(uuid, should_dedupe)
- url_path = get_url(name, uuid, should_dedupe)
-
- File.mkdir_p!(upload_folder)
+ def get_file(_) do
+ {:ok, {:static_dir, upload_path()}}
+ end
- result_file = Path.join(upload_folder, name)
+ def put_file(upload) do
+ {local_path, file} =
+ case Enum.reverse(String.split(upload.path, "/", trim: true)) do
+ [file] ->
+ {upload_path(), file}
- if File.exists?(result_file) do
- File.rm!(tmpfile)
- else
- File.cp!(tmpfile, result_file)
- end
+ [file | folders] ->
+ path = Path.join([upload_path()] ++ Enum.reverse(folders))
+ File.mkdir_p!(path)
+ {path, file}
+ end
- {:ok, url_path}
- end
+ result_file = Path.join(local_path, file)
- def upload_path do
- settings = Application.get_env(:pleroma, Pleroma.Uploaders.Local)
- Keyword.fetch!(settings, :uploads)
- end
-
- defp get_upload_path(uuid, should_dedupe) do
- if should_dedupe do
- upload_path()
- else
- Path.join(upload_path(), uuid)
+ unless File.exists?(result_file) do
+ File.cp!(upload.tempfile, result_file)
end
- end
- defp get_url(name, uuid, should_dedupe) do
- if should_dedupe do
- url_for(:cow_uri.urlencode(name))
- else
- url_for(Path.join(uuid, :cow_uri.urlencode(name)))
- end
+ :ok
end
- defp url_for(file) do
- settings = Application.get_env(:pleroma, Pleroma.Uploaders.Local)
-
- Keyword.get(settings, :uploads_url)
- |> String.replace("{{file}}", file)
- |> String.replace("{{base_url}}", Web.base_url())
+ def upload_path do
+ Pleroma.Config.get!([__MODULE__, :uploads])
end
end
diff --git a/lib/pleroma/uploaders/mdii.ex b/lib/pleroma/uploaders/mdii.ex
index a9d52b0dc..820cf88f5 100644
--- a/lib/pleroma/uploaders/mdii.ex
+++ b/lib/pleroma/uploaders/mdii.ex
@@ -5,22 +5,27 @@ defmodule Pleroma.Uploaders.MDII do
@httpoison Application.get_env(:pleroma, :httpoison)
- def put_file(name, uuid, path, content_type, should_dedupe) do
+ # MDII-hosted images are never passed through the MediaPlug; only local media.
+ # Delegate to Pleroma.Uploaders.Local
+ def get_file(file) do
+ Pleroma.Uploaders.Local.get_file(file)
+ end
+
+ def put_file(upload) do
cgi = Pleroma.Config.get([Pleroma.Uploaders.MDII, :cgi])
files = Pleroma.Config.get([Pleroma.Uploaders.MDII, :files])
- {:ok, file_data} = File.read(path)
+ {:ok, file_data} = File.read(upload.tempfile)
- extension = String.split(name, ".") |> List.last()
+ extension = String.split(upload.name, ".") |> List.last()
query = "#{cgi}?#{extension}"
- with {:ok, %{status_code: 200, body: body}} <- @httpoison.post(query, file_data) do
- File.rm!(path)
+ with {:ok, %{status: 200, body: body}} <- @httpoison.post(query, file_data) do
remote_file_name = String.split(body) |> List.first()
public_url = "#{files}/#{remote_file_name}.#{extension}"
- {:ok, public_url}
+ {:ok, {:url, public_url}}
else
- _ -> Pleroma.Uploaders.Local.put_file(name, uuid, path, content_type, should_dedupe)
+ _ -> Pleroma.Uploaders.Local.put_file(upload)
end
end
end
diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex
index 40a836460..19832a7ec 100644
--- a/lib/pleroma/uploaders/s3.ex
+++ b/lib/pleroma/uploaders/s3.ex
@@ -1,40 +1,46 @@
defmodule Pleroma.Uploaders.S3 do
- alias Pleroma.Web.MediaProxy
-
@behaviour Pleroma.Uploaders.Uploader
+ require Logger
+
+ # The file name is re-encoded with S3's constraints here to comply with previous links with less strict filenames
+ def get_file(file) do
+ config = Pleroma.Config.get([__MODULE__])
+
+ {:ok,
+ {:url,
+ Path.join([
+ Keyword.fetch!(config, :public_endpoint),
+ Keyword.fetch!(config, :bucket),
+ strict_encode(URI.decode(file))
+ ])}}
+ end
- def put_file(name, uuid, path, content_type, _should_dedupe) do
- settings = Application.get_env(:pleroma, Pleroma.Uploaders.S3)
- bucket = Keyword.fetch!(settings, :bucket)
- public_endpoint = Keyword.fetch!(settings, :public_endpoint)
- force_media_proxy = Keyword.fetch!(settings, :force_media_proxy)
-
- {:ok, file_data} = File.read(path)
+ def put_file(upload = %Pleroma.Upload{}) do
+ config = Pleroma.Config.get([__MODULE__])
+ bucket = Keyword.get(config, :bucket)
- File.rm!(path)
+ {:ok, file_data} = File.read(upload.tempfile)
- s3_name = "#{uuid}/#{encode(name)}"
+ s3_name = strict_encode(upload.path)
- {:ok, _} =
+ op =
ExAws.S3.put_object(bucket, s3_name, file_data, [
{:acl, :public_read},
- {:content_type, content_type}
+ {:content_type, upload.content_type}
])
- |> ExAws.request()
-
- url_base = "#{public_endpoint}/#{bucket}/#{s3_name}"
- public_url =
- if force_media_proxy do
- MediaProxy.url(url_base)
- else
- url_base
- end
+ case ExAws.request(op) do
+ {:ok, _} ->
+ {:ok, {:file, s3_name}}
- {:ok, public_url}
+ error ->
+ Logger.error("#{__MODULE__}: #{inspect(error)}")
+ {:error, "S3 Upload failed"}
+ end
end
- defp encode(name) do
- String.replace(name, ~r/[^0-9a-zA-Z!.*'()_-]/, "-")
+ @regex Regex.compile!("[^0-9a-zA-Z!.*/'()_-]")
+ def strict_encode(name) do
+ String.replace(name, @regex, "-")
end
end
diff --git a/lib/pleroma/uploaders/swift/keystone.ex b/lib/pleroma/uploaders/swift/keystone.ex
index e578b3c61..4aed977b1 100644
--- a/lib/pleroma/uploaders/swift/keystone.ex
+++ b/lib/pleroma/uploaders/swift/keystone.ex
@@ -25,10 +25,10 @@ defmodule Pleroma.Uploaders.Swift.Keystone do
["Content-Type": "application/json"],
hackney: [:insecure]
) do
- {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ {:ok, %Tesla.Env{status: 200, body: body}} ->
body["access"]["token"]["id"]
- {:ok, %HTTPoison.Response{status_code: _}} ->
+ {:ok, %Tesla.Env{status: _}} ->
""
end
end
diff --git a/lib/pleroma/uploaders/swift/swift.ex b/lib/pleroma/uploaders/swift/swift.ex
index fa08ca966..a5b3d2852 100644
--- a/lib/pleroma/uploaders/swift/swift.ex
+++ b/lib/pleroma/uploaders/swift/swift.ex
@@ -13,10 +13,10 @@ defmodule Pleroma.Uploaders.Swift.Client do
token = Pleroma.Uploaders.Swift.Keystone.get_token()
case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do
- {:ok, %HTTPoison.Response{status_code: 201}} ->
- {:ok, "#{object_url}/#{filename}"}
+ {:ok, %Tesla.Env{status: 201}} ->
+ {:ok, {:file, filename}}
- {:ok, %HTTPoison.Response{status_code: 401}} ->
+ {:ok, %Tesla.Env{status: 401}} ->
{:error, "Unauthorized, Bad Token"}
{:error, _} ->
diff --git a/lib/pleroma/uploaders/swift/uploader.ex b/lib/pleroma/uploaders/swift/uploader.ex
index 794f76cb0..b35b9807b 100644
--- a/lib/pleroma/uploaders/swift/uploader.ex
+++ b/lib/pleroma/uploaders/swift/uploader.ex
@@ -1,10 +1,15 @@
defmodule Pleroma.Uploaders.Swift do
@behaviour Pleroma.Uploaders.Uploader
- def put_file(name, uuid, tmp_path, content_type, _should_dedupe) do
- {:ok, file_data} = File.read(tmp_path)
- remote_name = "#{uuid}/#{name}"
+ def get_file(name) do
+ {:ok, {:url, Path.join([Pleroma.Config.get!([__MODULE__, :object_url]), name])}}
+ end
- Pleroma.Uploaders.Swift.Client.upload_file(remote_name, file_data, content_type)
+ def put_file(upload) do
+ Pleroma.Uploaders.Swift.Client.upload_file(
+ upload.path,
+ File.read!(upload.tmpfile),
+ upload.content_type
+ )
end
end
diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex
index b58fc6d71..afda5609e 100644
--- a/lib/pleroma/uploaders/uploader.ex
+++ b/lib/pleroma/uploaders/uploader.ex
@@ -1,20 +1,40 @@
defmodule Pleroma.Uploaders.Uploader do
@moduledoc """
- Defines the contract to put an uploaded file to any backend.
+ 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 `Pleroma.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, String.t } | {:error, String.t} containing the path of the
- uploaded file, or error information if the file failed to be saved to the
- respective 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.
+
+
"""
- @callback put_file(
- name :: String.t(),
- uuid :: String.t(),
- file :: File.t(),
- content_type :: String.t(),
- should_dedupe :: Boolean.t()
- ) :: {:ok, String.t()} | {:error, String.t()}
+ @callback put_file(Pleroma.Upload.t()) ::
+ :ok | {:ok, {:file | :url, String.t()}} | {:error, String.t()}
+
+ @spec put_file(module(), Pleroma.Upload.t()) ::
+ {:ok, {:file | :url, String.t()}} | {:error, String.t()}
+ def put_file(uploader, upload) do
+ case uploader.put_file(upload) do
+ :ok -> {:ok, {:file, upload.path}}
+ other -> other
+ end
+ end
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 6e1d5559d..74ae5ef0d 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -4,6 +4,8 @@ defmodule Pleroma.User do
import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2
+ alias Pleroma.Formatter
+ alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.{OStatus, Websub, OAuth}
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
@@ -19,11 +21,11 @@ defmodule Pleroma.User do
field(:ap_id, :string)
field(:avatar, :map)
field(:local, :boolean, default: true)
- field(:info, :map, default: %{})
field(:follower_address, :string)
field(:search_distance, :float, virtual: true)
field(:last_refreshed_at, :naive_datetime)
has_many(:notifications, Notification)
+ embeds_one(:info, Pleroma.User.Info)
timestamps()
end
@@ -36,13 +38,13 @@ defmodule Pleroma.User do
end
def banner_url(user) do
- case user.info["banner"] do
+ case user.info.banner do
%{"url" => [%{"href" => href} | _]} -> href
_ -> "#{Web.base_url()}/images/banner.png"
end
end
- def profile_url(%User{info: %{"source_data" => %{"url" => url}}}), do: url
+ def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
def profile_url(%User{ap_id: ap_id}), do: ap_id
def profile_url(_), do: nil
@@ -61,9 +63,7 @@ defmodule Pleroma.User do
end
def info_changeset(struct, params \\ %{}) do
- struct
- |> cast(params, [:info])
- |> validate_required([:info])
+ raise "NOT VALID ANYMORE"
end
def user_info(%User{} = user) do
@@ -71,27 +71,34 @@ defmodule Pleroma.User do
%{
following_count: length(user.following) - oneself,
- note_count: user.info["note_count"] || 0,
- follower_count: user.info["follower_count"] || 0,
- locked: user.info["locked"] || false,
- default_scope: user.info["default_scope"] || "public"
+ note_count: user.info.note_count,
+ follower_count: user.info.follower_count,
+ locked: user.info.locked,
+ default_scope: user.info.default_scope
}
end
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
def remote_user_creation(params) do
+ params =
+ params
+ |> Map.put(:info, params[:info] || %{})
+
+ info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
+
changes =
%User{}
- |> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
+ |> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
|> put_change(:local, false)
+ |> put_embed(:info, info_cng)
if changes.valid? do
- case changes.changes[:info]["source_data"] do
+ case info_cng.changes[:source_data] do
%{"followers" => followers} ->
changes
|> put_change(:follower_address, followers)
@@ -109,7 +116,7 @@ defmodule Pleroma.User do
def update_changeset(struct, params \\ %{}) do
struct
- |> cast(params, [:bio, :name])
+ |> cast(params, [:bio, :name, :avatar])
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000)
@@ -121,12 +128,17 @@ defmodule Pleroma.User do
params
|> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
+ info_cng =
+ struct.info
+ |> User.Info.user_upgrade(params[:info])
+
struct
- |> cast(params, [:bio, :name, :info, :follower_address, :avatar, :last_refreshed_at])
+ |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
+ |> put_embed(:info, info_cng)
end
def password_update_changeset(struct, params) do
@@ -165,6 +177,7 @@ defmodule Pleroma.User do
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000)
|> validate_length(:name, min: 1, max: 100)
+ |> put_change(:info, %Pleroma.User.Info{})
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
@@ -191,7 +204,7 @@ defmodule Pleroma.User do
def needs_update?(_), do: true
- def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{"locked" => true}}) do
+ def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
{:ok, follower}
end
@@ -222,7 +235,7 @@ defmodule Pleroma.User do
ap_followers = followed.follower_address
cond do
- following?(follower, followed) or info["deactivated"] ->
+ following?(follower, followed) or info.deactivated ->
{:error, "Could not follow user: #{followed.nickname} is already on your list."}
deny_follow_blocked and blocks?(followed, follower) ->
@@ -274,7 +287,7 @@ defmodule Pleroma.User do
end
def locked?(%User{} = user) do
- user.info["locked"] || false
+ user.info.locked || false
end
def get_by_ap_id(ap_id) do
@@ -411,22 +424,23 @@ defmodule Pleroma.User do
end
def increase_note_count(%User{} = user) do
- note_count = (user.info["note_count"] || 0) + 1
- new_info = Map.put(user.info, "note_count", note_count)
+ info_cng = User.Info.add_to_note_count(user.info, 1)
- cs = info_changeset(user, %{info: new_info})
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
def decrease_note_count(%User{} = user) do
- note_count = user.info["note_count"] || 0
- note_count = if note_count <= 0, do: 0, else: note_count - 1
- new_info = Map.put(user.info, "note_count", note_count)
+ info_cng = User.Info.add_to_note_count(user.info, -1)
- cs = info_changeset(user, %{info: new_info})
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
def update_note_count(%User{} = user) do
@@ -439,11 +453,13 @@ defmodule Pleroma.User do
note_count = Repo.one(note_count_query)
- new_info = Map.put(user.info, "note_count", note_count)
+ info_cng = User.Info.set_note_count(user.info, note_count)
- cs = info_changeset(user, %{info: new_info})
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
def update_follower_count(%User{} = user) do
@@ -457,11 +473,15 @@ defmodule Pleroma.User do
follower_count = Repo.one(follower_count_query)
- new_info = Map.put(user.info, "follower_count", follower_count)
+ info_cng =
+ user.info
+ |> User.Info.set_follower_count(follower_count)
- cs = info_changeset(user, %{info: new_info})
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
def get_users_from_set_query(ap_ids, false) do
@@ -545,12 +565,15 @@ defmodule Pleroma.User do
unfollow(blocked, blocker)
end
- blocks = blocker.info["blocks"] || []
- new_blocks = Enum.uniq([ap_id | blocks])
- new_info = Map.put(blocker.info, "blocks", new_blocks)
+ info_cng =
+ blocker.info
+ |> User.Info.add_to_block(ap_id)
+
+ cng =
+ change(blocker)
+ |> put_embed(:info, info_cng)
- cs = User.info_changeset(blocker, %{info: new_info})
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
# helper to handle the block given only an actor's AP id
@@ -558,18 +581,21 @@ defmodule Pleroma.User do
block(blocker, User.get_by_ap_id(ap_id))
end
- def unblock(user, %{ap_id: ap_id}) do
- blocks = user.info["blocks"] || []
- new_blocks = List.delete(blocks, ap_id)
- new_info = Map.put(user.info, "blocks", new_blocks)
+ def unblock(blocker, %{ap_id: ap_id}) do
+ info_cng =
+ blocker.info
+ |> User.Info.remove_from_block(ap_id)
- cs = User.info_changeset(user, %{info: new_info})
- update_and_set_cache(cs)
+ cng =
+ change(blocker)
+ |> put_embed(:info, info_cng)
+
+ update_and_set_cache(cng)
end
def blocks?(user, %{ap_id: ap_id}) do
- blocks = user.info["blocks"] || []
- domain_blocks = user.info["domain_blocks"] || []
+ blocks = user.info.blocks
+ domain_blocks = user.info.domain_blocks
%{host: host} = URI.parse(ap_id)
Enum.member?(blocks, ap_id) ||
@@ -579,21 +605,27 @@ defmodule Pleroma.User do
end
def block_domain(user, domain) do
- domain_blocks = user.info["domain_blocks"] || []
- new_blocks = Enum.uniq([domain | domain_blocks])
- new_info = Map.put(user.info, "domain_blocks", new_blocks)
+ info_cng =
+ user.info
+ |> User.Info.add_to_domain_block(domain)
+
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
- cs = User.info_changeset(user, %{info: new_info})
- update_and_set_cache(cs)
+ update_and_set_cache(cng)
end
def unblock_domain(user, domain) do
- blocks = user.info["domain_blocks"] || []
- new_blocks = List.delete(blocks, domain)
- new_info = Map.put(user.info, "domain_blocks", new_blocks)
+ info_cng =
+ user.info
+ |> User.Info.remove_from_domain_block(domain)
- cs = User.info_changeset(user, %{info: new_info})
- update_and_set_cache(cs)
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
+
+ update_and_set_cache(cng)
end
def local_user_query() do
@@ -613,9 +645,13 @@ defmodule Pleroma.User do
end
def deactivate(%User{} = user, status \\ true) do
- new_info = Map.put(user.info, "deactivated", status)
- cs = User.info_changeset(user, %{info: new_info})
- update_and_set_cache(cs)
+ info_cng = User.Info.set_activation_status(user.info, status)
+
+ cng =
+ change(user)
+ |> put_embed(:info, info_cng)
+
+ update_and_set_cache(cng)
end
def delete(%User{} = user) do
@@ -649,7 +685,7 @@ defmodule Pleroma.User do
{:ok, user}
end
- def html_filter_policy(%User{info: %{"no_rich_text" => true}}) do
+ def html_filter_policy(%User{info: %{no_rich_text: true}}) do
Pleroma.HTML.Scrubber.TwitterText
end
@@ -683,7 +719,7 @@ defmodule Pleroma.User do
user
else
changes =
- %User{}
+ %User{info: %User.Info{}}
|> cast(%{}, [:ap_id, :nickname, :local])
|> put_change(:ap_id, relay_uri)
|> put_change(:nickname, nil)
@@ -697,7 +733,7 @@ defmodule Pleroma.User do
# AP style
def public_key_from_info(%{
- "source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
+ source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
}) do
key =
:public_key.pem_decode(public_key_pem)
@@ -708,7 +744,7 @@ defmodule Pleroma.User do
end
# OStatus Magic Key
- def public_key_from_info(%{"magic_key" => magic_key}) do
+ def public_key_from_info(%{magic_key: magic_key}) do
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
end
@@ -730,11 +766,12 @@ defmodule Pleroma.User do
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
cs = User.remote_user_creation(data)
+
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end
def ap_enabled?(%User{local: true}), do: true
- def ap_enabled?(%User{info: info}), do: info["ap_enabled"]
+ def ap_enabled?(%User{info: info}), do: info.ap_enabled
def ap_enabled?(_), do: false
def get_or_fetch(uri_or_nickname) do
@@ -768,4 +805,18 @@ defmodule Pleroma.User do
:error
end
end
+
+ def parse_bio(bio, user \\ %User{info: %{source_data: %{}}}) do
+ mentions = Formatter.parse_mentions(bio)
+ tags = Formatter.parse_tags(bio)
+
+ emoji =
+ (user.info.source_data["tag"] || [])
+ |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
+ |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
+ {String.trim(name, ":"), url}
+ end)
+
+ CommonUtils.format_input(bio, mentions, tags, "text/plain") |> Formatter.emojify(emoji)
+ end
end
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
new file mode 100644
index 000000000..7a99787f8
--- /dev/null
+++ b/lib/pleroma/user/info.ex
@@ -0,0 +1,167 @@
+defmodule Pleroma.User.Info do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ embedded_schema do
+ field(:banner, :map, default: %{})
+ field(:background, :map, default: %{})
+ field(:source_data, :map, default: %{})
+ field(:note_count, :integer, default: 0)
+ field(:follower_count, :integer, default: 0)
+ field(:locked, :boolean, default: false)
+ field(:default_scope, :string, default: "public")
+ field(:blocks, {:array, :string}, default: [])
+ field(:domain_blocks, {:array, :string}, default: [])
+ field(:deactivated, :boolean, default: false)
+ field(:no_rich_text, :boolean, default: false)
+ field(:ap_enabled, :boolean, default: false)
+ field(:is_moderator, :boolean, default: false)
+ field(:is_admin, :boolean, default: false)
+ field(:keys, :string, default: nil)
+ field(:settings, :map, default: nil)
+ field(:magic_key, :string, default: nil)
+ field(:uri, :string, default: nil)
+ field(:topic, :string, default: nil)
+ field(:hub, :string, default: nil)
+ field(:salmon, :string, default: nil)
+ field(:hide_network, :boolean, default: false)
+
+ # Found in the wild
+ # ap_id -> Where is this used?
+ # bio -> Where is this used?
+ # avatar -> Where is this used?
+ # fqn -> Where is this used?
+ # host -> Where is this used?
+ # subject _> Where is this used?
+ end
+
+ def set_activation_status(info, deactivated) do
+ params = %{deactivated: deactivated}
+
+ info
+ |> cast(params, [:deactivated])
+ |> validate_required([:deactivated])
+ end
+
+ def add_to_note_count(info, number) do
+ set_note_count(info, info.note_count + number)
+ end
+
+ def set_note_count(info, number) do
+ params = %{note_count: Enum.max([0, number])}
+
+ info
+ |> cast(params, [:note_count])
+ |> validate_required([:note_count])
+ end
+
+ def set_follower_count(info, number) do
+ params = %{follower_count: Enum.max([0, number])}
+
+ info
+ |> cast(params, [:follower_count])
+ |> validate_required([:follower_count])
+ end
+
+ def set_blocks(info, blocks) do
+ params = %{blocks: blocks}
+
+ info
+ |> cast(params, [:blocks])
+ |> validate_required([:blocks])
+ end
+
+ def add_to_block(info, blocked) do
+ set_blocks(info, Enum.uniq([blocked | info.blocks]))
+ end
+
+ def remove_from_block(info, blocked) do
+ set_blocks(info, List.delete(info.blocks, blocked))
+ end
+
+ def set_domain_blocks(info, domain_blocks) do
+ params = %{domain_blocks: domain_blocks}
+
+ info
+ |> cast(params, [:domain_blocks])
+ |> validate_required([:domain_blocks])
+ end
+
+ def add_to_domain_block(info, domain_blocked) do
+ set_domain_blocks(info, Enum.uniq([domain_blocked | info.domain_blocks]))
+ end
+
+ def remove_from_domain_block(info, domain_blocked) do
+ set_domain_blocks(info, List.delete(info.domain_blocks, domain_blocked))
+ end
+
+ def set_keys(info, keys) do
+ params = %{keys: keys}
+
+ info
+ |> cast(params, [:keys])
+ |> validate_required([:keys])
+ end
+
+ def remote_user_creation(info, params) do
+ info
+ |> cast(params, [
+ :ap_enabled,
+ :source_data,
+ :banner,
+ :locked,
+ :magic_key,
+ :uri,
+ :hub,
+ :topic,
+ :salmon
+ ])
+ end
+
+ def user_upgrade(info, params) do
+ info
+ |> cast(params, [
+ :ap_enabled,
+ :source_data,
+ :banner,
+ :locked,
+ :magic_key
+ ])
+ end
+
+ def profile_update(info, params) do
+ info
+ |> cast(params, [
+ :locked,
+ :no_rich_text,
+ :default_scope,
+ :banner,
+ :hide_network,
+ :background
+ ])
+ end
+
+ def mastodon_profile_update(info, params) do
+ info
+ |> cast(params, [
+ :locked,
+ :banner
+ ])
+ end
+
+ def set_source_data(info, source_data) do
+ params = %{source_data: source_data}
+
+ info
+ |> cast(params, [:source_data])
+ |> validate_required([:source_data])
+ end
+
+ def admin_api_update(info, params) do
+ info
+ |> cast(params, [
+ :is_moderator,
+ :is_admin
+ ])
+ end
+end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index ed579e336..28da57a10 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -42,7 +42,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp check_actor_is_active(actor) do
if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor),
- false <- !!user.info["deactivated"] do
+ false <- user.info.deactivated do
:ok
else
_e -> :reject
@@ -509,8 +509,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
- blocks = info["blocks"] || []
- domain_blocks = info["domain_blocks"] || []
+ blocks = info.blocks || []
+ domain_blocks = info.domain_blocks || []
from(
activity in query,
@@ -572,11 +572,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.reverse()
end
- def upload(file, size_limit \\ nil) do
- with data <-
- Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media], size_limit),
- false <- is_nil(data) do
- Repo.insert(%Object{data: data})
+ def upload(file, opts \\ []) do
+ with {:ok, data} <- Upload.store(file, opts) do
+ obj_data =
+ if opts[:actor] do
+ Map.put(data, "actor", opts[:actor])
+ else
+ data
+ end
+
+ Repo.insert(%Object{data: obj_data})
end
end
@@ -678,7 +683,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
remote_inboxes =
(Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
- |> Enum.map(fn %{info: %{"source_data" => data}} ->
+ |> Enum.map(fn %{info: %{source_data: data}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
@@ -764,7 +769,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Logger.info("Fetching #{id} via AP")
with true <- String.starts_with?(id, "http"),
- {:ok, %{body: body, status_code: code}} when code in 200..299 <-
+ {:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
id,
[Accept: "application/activity+json"],
diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
index 86dcf5080..12fc3b181 100644
--- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
+++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex
@@ -23,7 +23,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_media_removal(
%{host: actor_host} = _actor_info,
- %{"type" => "Create", "object" => %{"attachement" => child_attachment}} = object
+ %{"type" => "Create", "object" => %{"attachment" => child_attachment}} = object
)
when length(child_attachment) > 0 do
object =
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 5864855b0..17b063609 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -447,7 +447,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
update_data =
new_user_data
|> Map.take([:name, :bio, :avatar])
- |> Map.put(:info, Map.merge(actor.info, %{"banner" => banner, "locked" => locked}))
+ |> Map.put(:info, %{"banner" => banner, "locked" => locked})
actor
|> User.upgrade_changeset(update_data)
@@ -850,10 +850,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def upgrade_user_from_ap_id(ap_id, async \\ true) do
with %User{local: false} = user <- User.get_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
- data =
- data
- |> Map.put(:info, Map.merge(user.info, data[:info]))
-
already_ap = User.ap_enabled?(user)
{:ok, user} =
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index eb335813d..869934172 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
# the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %{nickname: nil} = user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
- {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
+ {:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
@@ -40,7 +40,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
def render("user.json", %{user: user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
- {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
+ {:ok, _, public_key} = Salmon.keys_from_pem(user.info.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
@@ -55,7 +55,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"name" => user.name,
"summary" => user.bio,
"url" => user.ap_id,
- "manuallyApprovesFollowers" => user.info["locked"] || false,
+ "manuallyApprovesFollowers" => user.info.locked,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
@@ -72,7 +72,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"type" => "Image",
"url" => User.banner_url(user)
},
- "tag" => user.info["source_data"]["tag"] || []
+ "tag" => user.info.source_data["tag"] || []
}
|> Map.merge(Utils.make_json_ld_header())
end
@@ -82,7 +82,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
- collection(following, "#{user.ap_id}/following", page)
+ collection(following, "#{user.ap_id}/following", page, !user.info.hide_network)
|> Map.merge(Utils.make_json_ld_header())
end
@@ -95,7 +95,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"id" => "#{user.ap_id}/following",
"type" => "OrderedCollection",
"totalItems" => length(following),
- "first" => collection(following, "#{user.ap_id}/following", 1)
+ "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_network)
}
|> Map.merge(Utils.make_json_ld_header())
end
@@ -105,7 +105,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
- collection(followers, "#{user.ap_id}/followers", page)
+ collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_network)
|> Map.merge(Utils.make_json_ld_header())
end
@@ -118,7 +118,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"id" => "#{user.ap_id}/followers",
"type" => "OrderedCollection",
"totalItems" => length(followers),
- "first" => collection(followers, "#{user.ap_id}/followers", 1)
+ "first" => collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_network)
}
|> Map.merge(Utils.make_json_ld_header())
end
@@ -172,7 +172,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
end
end
- def collection(collection, iri, page, total \\ nil) do
+ def collection(collection, iri, page, show_items \\ true, total \\ nil) do
offset = (page - 1) * 10
items = Enum.slice(collection, offset, 10)
items = Enum.map(items, fn user -> user.ap_id end)
@@ -183,7 +183,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => total,
- "orderedItems" => items
+ "orderedItems" => if(show_items, do: items, else: [])
}
if offset < total do
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index bcdb4ba37..2c67d9cda 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -45,21 +45,29 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
user = User.get_by_nickname(nickname)
info =
- user.info
+ %{}
|> Map.put("is_" <> permission_group, true)
- cng = User.info_changeset(user, %{info: info})
+ info_cng = User.Info.admin_api_update(user.info, info)
+
+ cng =
+ Ecto.Changeset.change(user)
+ |> Ecto.Changeset.put_embed(:info, info_cng)
+
{:ok, user} = User.update_and_set_cache(cng)
conn
- |> json(user.info)
+ |> json(info)
end
def right_get(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname)
conn
- |> json(user.info)
+ |> json(%{
+ is_moderator: user.info.is_moderator,
+ is_admin: user.info.is_admin
+ })
end
def right_add(conn, _) do
@@ -84,14 +92,19 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
user = User.get_by_nickname(nickname)
info =
- user.info
+ %{}
|> Map.put("is_" <> permission_group, false)
- cng = User.info_changeset(user, %{info: info})
+ info_cng = User.Info.admin_api_update(user.info, info)
+
+ cng =
+ Ecto.Changeset.change(user)
+ |> Ecto.Changeset.put_embed(:info, info_cng)
+
{:ok, user} = User.update_and_set_cache(cng)
conn
- |> json(user.info)
+ |> json(info)
end
end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 77e4dbbd7..e3385310f 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.CommonAPI do
def delete(activity_id, user) do
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),
%Object{} = object <- Object.normalize(object_id),
- true <- user.info["is_moderator"] || user.ap_id == object.data["actor"],
+ true <- user.info.is_moderator || user.ap_id == object.data["actor"],
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
end
@@ -135,12 +135,13 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ # Updates the emojis for a user based on their profile
def update(user) do
user =
with emoji <- emoji_from_profile(user),
- source_data <- (user.info["source_data"] || %{}) |> Map.put("tag", emoji),
- new_info <- Map.put(user.info, "source_data", source_data),
- change <- User.info_changeset(user, %{info: new_info}),
+ source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
+ info_cng <- Pleroma.User.Info.set_source_data(user.info, source_data),
+ change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index 8728c908b..c5f9d51d9 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -12,7 +12,7 @@ defmodule Pleroma.Web.Endpoint do
plug(CORSPlug)
plug(Pleroma.Plugs.HTTPSecurityPlug)
- plug(Plug.Static, at: "/media", from: Pleroma.Uploaders.Local.upload_path(), gzip: false)
+ plug(Pleroma.Plugs.UploadedMedia)
plug(
Plug.Static,
diff --git a/lib/pleroma/web/federator/retry_queue.ex b/lib/pleroma/web/federator/retry_queue.ex
index 06c094f26..13df40c80 100644
--- a/lib/pleroma/web/federator/retry_queue.ex
+++ b/lib/pleroma/web/federator/retry_queue.ex
@@ -17,7 +17,15 @@ defmodule Pleroma.Web.Federator.RetryQueue do
end
def start_link() do
- GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__)
+ enabled = Pleroma.Config.get([:retry_queue, :enabled], false)
+
+ if enabled do
+ Logger.info("Starting retry queue")
+ GenServer.start_link(__MODULE__, %{delivered: 0, dropped: 0}, name: __MODULE__)
+ else
+ Logger.info("Retry queue disabled")
+ :ignore
+ end
end
def enqueue(data, transport, retries \\ 0) do
diff --git a/lib/pleroma/web/http_signatures/http_signatures.ex b/lib/pleroma/web/http_signatures/http_signatures.ex
index 5e42a871b..0e54debd5 100644
--- a/lib/pleroma/web/http_signatures/http_signatures.ex
+++ b/lib/pleroma/web/http_signatures/http_signatures.ex
@@ -65,7 +65,7 @@ defmodule Pleroma.Web.HTTPSignatures do
end
def sign(user, headers) do
- with {:ok, %{info: %{"keys" => keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
+ with {:ok, %{info: %{keys: keys}}} <- Pleroma.Web.WebFinger.ensure_keys_present(user),
{:ok, private_key, _} = Pleroma.Web.Salmon.keys_from_pem(keys) do
sigstring = build_signing_string(headers, Map.keys(headers))
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index f5b23971e..dd6b0a361 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -32,75 +32,55 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
- def update_credentials(%{assigns: %{user: user}} = conn, params) do
- original_user = user
-
- avatar_upload_limit =
- Application.get_env(:pleroma, :instance)
- |> Keyword.fetch(:avatar_upload_limit)
-
- banner_upload_limit =
- Application.get_env(:pleroma, :instance)
- |> Keyword.fetch(:banner_upload_limit)
-
- params =
- if bio = params["note"] do
- Map.put(params, "bio", bio)
- else
- params
+ defp add_if_present(
+ map,
+ params,
+ params_field,
+ map_field,
+ value_function \\ fn x -> {:ok, x} end
+ ) do
+ if Map.has_key?(params, params_field) do
+ case value_function.(params[params_field]) do
+ {:ok, new_value} -> Map.put(map, map_field, new_value)
+ :error -> map
end
+ else
+ map
+ end
+ end
- params =
- if name = params["display_name"] do
- Map.put(params, "name", name)
- else
- params
- end
+ def update_credentials(%{assigns: %{user: user}} = conn, params) do
+ original_user = user
- user =
- if avatar = params["avatar"] do
- with %Plug.Upload{} <- avatar,
- {:ok, object} <- ActivityPub.upload(avatar, avatar_upload_limit),
- change = Ecto.Changeset.change(user, %{avatar: object.data}),
- {:ok, user} = User.update_and_set_cache(change) do
- user
+ user_params =
+ %{}
+ |> add_if_present(params, "display_name", :name)
+ |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
+ |> add_if_present(params, "avatar", :avatar, fn value ->
+ with %Plug.Upload{} <- value,
+ {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
+ {:ok, object.data}
else
- _e -> user
+ _ -> :error
end
- else
- user
- end
+ end)
- user =
- if banner = params["header"] do
- with %Plug.Upload{} <- banner,
- {:ok, object} <- ActivityPub.upload(banner, banner_upload_limit),
- new_info <- Map.put(user.info, "banner", object.data),
- change <- User.info_changeset(user, %{info: new_info}),
- {:ok, user} <- User.update_and_set_cache(change) do
- user
+ info_params =
+ %{}
+ |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
+ |> add_if_present(params, "header", :banner, fn value ->
+ with %Plug.Upload{} <- value,
+ {:ok, object} <- ActivityPub.upload(value, type: :banner) do
+ {:ok, object.data}
else
- _e -> user
+ _ -> :error
end
- else
- user
- end
+ end)
- user =
- if locked = params["locked"] do
- with locked <- locked == "true",
- new_info <- Map.put(user.info, "locked", locked),
- change <- User.info_changeset(user, %{info: new_info}),
- {:ok, user} <- User.update_and_set_cache(change) do
- user
- else
- _e -> user
- end
- else
- user
- end
+ info_cng = User.Info.mastodon_profile_update(user.info, info_params)
- with changeset <- User.update_changeset(user, params),
+ with changeset <- User.update_changeset(user, user_params),
+ changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user do
CommonAPI.update(user)
@@ -453,40 +433,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> json([])
end
- def update_media(%{assigns: %{user: _}} = conn, data) do
+ def update_media(%{assigns: %{user: user}} = conn, data) do
with %Object{} = object <- Repo.get(Object, data["id"]),
+ true <- Object.authorize_mutation(object, user),
true <- is_binary(data["description"]),
description <- data["description"] do
new_data = %{object.data | "name" => description}
- change = Object.change(object, %{data: new_data})
- {:ok, _} = Repo.update(change)
-
- data =
- new_data
- |> Map.put("id", object.id)
+ {:ok, _} =
+ object
+ |> Object.change(%{data: new_data})
+ |> Repo.update()
- render(conn, StatusView, "attachment.json", %{attachment: data})
+ attachment_data = Map.put(new_data, "id", object.id)
+ render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
end
end
- def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do
- with {:ok, object} <- ActivityPub.upload(file) do
- objdata =
- if Map.has_key?(data, "description") do
- Map.put(object.data, "name", data["description"])
- else
- object.data
- end
-
- change = Object.change(object, %{data: objdata})
- {:ok, object} = Repo.update(change)
-
- objdata =
- objdata
- |> Map.put("id", object.id)
-
- render(conn, StatusView, "attachment.json", %{attachment: objdata})
+ def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+ with {:ok, object} <-
+ ActivityPub.upload(file,
+ actor: User.ap_id(user),
+ description: Map.get(data, "description")
+ ) do
+ attachment_data = Map.put(object.data, "id", object.id)
+ render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
end
end
@@ -529,17 +500,30 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
- # TODO: Pagination
- def followers(conn, %{"id" => id}) do
+ def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_followers(user) do
+ followers =
+ cond do
+ for_user && user.id == for_user.id -> followers
+ user.info.hide_network -> []
+ true -> followers
+ end
+
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end
end
- def following(conn, %{"id" => id}) do
+ def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_friends(user) do
+ followers =
+ cond do
+ for_user && user.id == for_user.id -> followers
+ user.info.hide_network -> []
+ true -> followers
+ end
+
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end
end
@@ -659,7 +643,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
# TODO: Use proper query
def blocks(%{assigns: %{user: user}} = conn, _) do
- with blocked_users <- user.info["blocks"] || [],
+ with blocked_users <- user.info.blocks || [],
accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
@@ -667,7 +651,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
- json(conn, info["domain_blocks"] || [])
+ json(conn, info.domain_blocks || [])
end
def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
@@ -915,11 +899,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
max_toot_chars: limit
},
rights: %{
- delete_others_notice: !!user.info["is_moderator"]
+ delete_others_notice: !!user.info.is_moderator
},
compose: %{
me: "#{user.id}",
- default_privacy: user.info["default_scope"] || "public",
+ default_privacy: user.info.default_scope,
default_sensitive: false
},
media_attachments: %{
@@ -939,7 +923,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
]
},
settings:
- Map.get(user.info, "settings") ||
+ Map.get(user.info, :settings) ||
%{
onboarded: true,
home: %{
@@ -1224,7 +1208,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
user = user.nickname
url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
- with {:ok, %{status_code: 200, body: body}} <-
+ with {:ok, %{status: 200, body: body}} <-
@httpoison.get(url, [], timeout: timeout, recv_timeout: timeout),
{:ok, data} <- Jason.decode(body) do
data2 =
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index b68845e16..bcfa8836e 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -14,10 +14,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url()
user_info = User.user_info(user)
- bot = (user.info["source_data"]["type"] || "Person") in ["Application", "Service"]
+ bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]
emojis =
- (user.info["source_data"]["tag"] || [])
+ (user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
%{
@@ -29,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end)
fields =
- (user.info["source_data"]["attachment"] || [])
+ (user.info.source_data["attachment"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
diff --git a/lib/pleroma/web/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/controller.ex
index bb257c262..e1b87e026 100644
--- a/lib/pleroma/web/media_proxy/controller.ex
+++ b/lib/pleroma/web/media_proxy/controller.ex
@@ -1,135 +1,34 @@
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
use Pleroma.Web, :controller
- require Logger
+ alias Pleroma.{Web.MediaProxy, ReverseProxy}
- @httpoison Application.get_env(:pleroma, :httpoison)
+ @default_proxy_opts [max_body_length: 25 * 1_048_576]
- @max_body_length 25 * 1_048_576
-
- @cache_control %{
- default: "public, max-age=1209600",
- error: "public, must-revalidate, max-age=160"
- }
-
- # Content-types that will not be returned as content-disposition attachments
- # Override with :media_proxy, :safe_content_types in the configuration
- @safe_content_types [
- "image/gif",
- "image/jpeg",
- "image/jpg",
- "image/png",
- "image/svg+xml",
- "audio/mpeg",
- "audio/mp3",
- "video/webm",
- "video/mp4"
- ]
-
- def remote(conn, params = %{"sig" => sig, "url" => url}) do
- config = Application.get_env(:pleroma, :media_proxy, [])
-
- with true <- Keyword.get(config, :enabled, false),
- {:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
+ def remote(conn, params = %{"sig" => sig64, "url" => url64}) do
+ with config <- Pleroma.Config.get([:media_proxy], []),
+ true <- Keyword.get(config, :enabled, false),
+ {:ok, url} <- MediaProxy.decode_url(sig64, url64),
filename <- Path.basename(URI.parse(url).path),
- true <-
- if(Map.get(params, "filename"),
- do: filename == Path.basename(conn.request_path),
- else: true
- ),
- {:ok, content_type, body} <- proxy_request(url),
- safe_content_type <-
- Enum.member?(
- Keyword.get(config, :safe_content_types, @safe_content_types),
- content_type
- ) do
- conn
- |> put_resp_content_type(content_type)
- |> set_cache_header(:default)
- |> put_resp_header(
- "content-security-policy",
- "default-src 'none'; style-src 'unsafe-inline'; media-src data:; img-src 'self' data:"
- )
- |> put_resp_header("x-xss-protection", "1; mode=block")
- |> put_resp_header("x-content-type-options", "nosniff")
- |> put_attachement_header(safe_content_type, filename)
- |> send_resp(200, body)
+ :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_error(conn, 404)
+ send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404))
{:error, :invalid_signature} ->
- send_error(conn, 403)
+ send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403))
- {:error, {:http, _, url}} ->
- redirect_or_error(conn, url, Keyword.get(config, :redirect_on_failure, true))
+ {:wrong_filename, filename} ->
+ redirect(conn, external: MediaProxy.build_url(sig64, url64, filename))
end
end
- defp proxy_request(link) do
- headers = [
- {"user-agent",
- "Pleroma/MediaProxy; #{Pleroma.Web.base_url()} <#{
- Application.get_env(:pleroma, :instance)[:email]
- }>"}
- ]
-
- options =
- @httpoison.process_request_options([:insecure, {:follow_redirect, true}]) ++
- [{:pool, :default}]
-
- with {:ok, 200, headers, client} <- :hackney.request(:get, link, headers, "", options),
- headers = Enum.into(headers, Map.new()),
- {:ok, body} <- proxy_request_body(client),
- content_type <- proxy_request_content_type(headers, body) do
- {:ok, content_type, body}
- else
- {:ok, status, _, _} ->
- Logger.warn("MediaProxy: request failed, status #{status}, link: #{link}")
- {:error, {:http, :bad_status, link}}
+ def filename_matches(has_filename, path, url) do
+ filename = MediaProxy.filename(url)
- {:error, error} ->
- Logger.warn("MediaProxy: request failed, error #{inspect(error)}, link: #{link}")
- {:error, {:http, error, link}}
+ cond do
+ has_filename && filename && Path.basename(path) != filename -> {:wrong_filename, filename}
+ true -> :ok
end
end
-
- defp set_cache_header(conn, key) do
- Plug.Conn.put_resp_header(conn, "cache-control", @cache_control[key])
- end
-
- defp redirect_or_error(conn, url, true), do: redirect(conn, external: url)
- defp redirect_or_error(conn, url, _), do: send_error(conn, 502, "Media proxy error: " <> url)
-
- defp send_error(conn, code, body \\ "") do
- conn
- |> set_cache_header(:error)
- |> send_resp(code, body)
- end
-
- defp proxy_request_body(client), do: proxy_request_body(client, <<>>)
-
- defp proxy_request_body(client, body) when byte_size(body) < @max_body_length do
- case :hackney.stream_body(client) do
- {:ok, data} -> proxy_request_body(client, <<body::binary, data::binary>>)
- :done -> {:ok, body}
- {:error, reason} -> {:error, reason}
- end
- end
-
- defp proxy_request_body(client, _) do
- :hackney.close(client)
- {:error, :body_too_large}
- end
-
- # TODO: the body is passed here as well because some hosts do not provide a content-type.
- # At some point we may want to use magic numbers to discover the content-type and reply a proper one.
- defp proxy_request_content_type(headers, _body) do
- headers["Content-Type"] || headers["content-type"] || "application/octet-stream"
- end
-
- defp put_attachement_header(conn, true, _), do: conn
-
- defp put_attachement_header(conn, false, filename) do
- put_resp_header(conn, "content-disposition", "attachment; filename='#{filename}'")
- end
end
diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex
index 0fc0a07b2..28aacb0b1 100644
--- a/lib/pleroma/web/media_proxy/media_proxy.ex
+++ b/lib/pleroma/web/media_proxy/media_proxy.ex
@@ -17,10 +17,8 @@ defmodule Pleroma.Web.MediaProxy do
base64 = Base.url_encode64(url, @base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts)
- filename = if path = URI.parse(url).path, do: "/" <> Path.basename(path), else: ""
- Keyword.get(config, :base_url, Pleroma.Web.base_url()) <>
- "/proxy/#{sig64}/#{base64}#{filename}"
+ build_url(sig64, base64, filename(url))
end
end
@@ -35,4 +33,20 @@ defmodule Pleroma.Web.MediaProxy do
{: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
+ [
+ Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()),
+ "proxy",
+ sig_base64,
+ url_base64,
+ filename
+ ]
+ |> Enum.filter(fn value -> value end)
+ |> Path.join()
+ end
end
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index 1d0019d3b..67df354db 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -226,25 +226,21 @@ defmodule Pleroma.Web.OStatus do
old_data = %{
avatar: user.avatar,
bio: user.bio,
- name: user.name,
- info: user.info
+ name: user.name
}
with false <- user.local,
avatar <- make_avatar_object(doc),
bio <- string_from_xpath("//author[1]/summary", doc),
name <- string_from_xpath("//author[1]/poco:displayName", doc),
- info <-
- Map.put(user.info, "banner", make_avatar_object(doc, "header") || user.info["banner"]),
new_data <- %{
avatar: avatar || old_data.avatar,
name: name || old_data.name,
- bio: bio || old_data.bio,
- info: info || old_data.info
+ bio: bio || old_data.bio
},
false <- new_data == old_data do
change = Ecto.Changeset.change(user, new_data)
- Repo.update(change)
+ User.update_and_set_cache(change)
else
_ ->
{:ok, user}
@@ -350,13 +346,15 @@ defmodule Pleroma.Web.OStatus do
def fetch_activity_from_atom_url(url) do
with true <- String.starts_with?(url, "http"),
- {:ok, %{body: body, status_code: code}} when code in 200..299 <-
+ {:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
url,
[Accept: "application/atom+xml"],
follow_redirect: true,
- timeout: 10000,
- recv_timeout: 20000
+ adapter: [
+ timeout: 10000,
+ recv_timeout: 20000
+ ]
) do
Logger.debug("Got document from #{url}, handling...")
handle_incoming(body)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index d8af81992..75d965c6d 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -302,12 +302,6 @@ defmodule Pleroma.Web.Router do
post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
- post(
- "/account/most_recent_notification",
- TwitterAPI.Controller,
- :update_most_recent_notification
- )
-
get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
@@ -335,6 +329,7 @@ defmodule Pleroma.Web.Router do
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
post("/media/upload", TwitterAPI.Controller, :upload_json)
+ post("/media/metadata/create", TwitterAPI.Controller, :update_media)
post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
post("/favorites/create", TwitterAPI.Controller, :favorite)
diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex
index 562ec3d9c..97251c05e 100644
--- a/lib/pleroma/web/salmon/salmon.ex
+++ b/lib/pleroma/web/salmon/salmon.ex
@@ -157,15 +157,17 @@ defmodule Pleroma.Web.Salmon do
|> Enum.filter(fn user -> user && !user.local end)
end
- defp send_to_user(%{info: %{"salmon" => salmon}}, feed, poster) do
- with {:ok, %{status_code: code}} <-
+ defp send_to_user(%{info: %{salmon: salmon}}, feed, poster) do
+ with {:ok, %{status: code}} <-
poster.(
salmon,
feed,
[{"Content-Type", "application/magic-envelope+xml"}],
- timeout: 10000,
- recv_timeout: 20000,
- hackney: [pool: :default]
+ adapter: [
+ timeout: 10000,
+ recv_timeout: 20000,
+ pool: :default
+ ]
) do
Logger.debug(fn -> "Pushed to #{salmon}, code #{code}" end)
else
@@ -185,7 +187,7 @@ defmodule Pleroma.Web.Salmon do
]
def publish(user, activity, poster \\ &@httpoison.post/4)
- def publish(%{info: %{"keys" => keys}} = user, %{data: %{"type" => type}} = activity, poster)
+ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity, poster)
when type in @supported_activities do
feed = ActivityRepresenter.to_simple_form(activity, user, true)
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index 306598157..99b8b7063 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -188,7 +188,7 @@ defmodule Pleroma.Web.Streamer do
# Get the current user so we have up-to-date blocks etc.
if socket.assigns[:user] do
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
- blocks = user.info["blocks"] || []
+ blocks = user.info.blocks || []
parent = Object.normalize(item.data["object"])
@@ -206,7 +206,7 @@ defmodule Pleroma.Web.Streamer do
# Get the current user so we have up-to-date blocks etc.
if socket.assigns[:user] do
user = User.get_cached_by_ap_id(socket.assigns[:user].ap_id)
- blocks = user.info["blocks"] || []
+ blocks = user.info.blocks || []
unless item.actor in blocks do
send(socket.transport_pid, {:text, represent_update(item, user)})
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index 5bfb83b1e..9c485d965 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -93,11 +93,11 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
end
end
- def upload(%Plug.Upload{} = file, format \\ "xml") do
- {:ok, object} = ActivityPub.upload(file)
+ def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
+ {:ok, object} = ActivityPub.upload(file, actor: User.ap_id(user))
url = List.first(object.data["url"])
- href = url["href"] |> MediaProxy.url()
+ href = url["href"]
type = url["mediaType"]
case format do
@@ -132,7 +132,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
params = %{
nickname: params["nickname"],
name: params["fullname"],
- bio: params["bio"],
+ bio: User.parse_bio(params["bio"]),
email: params["email"],
password: params["password"],
password_confirmation: params["confirm"]
@@ -148,7 +148,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
cond do
registrations_open || (!is_nil(token) && !token.used) ->
- changeset = User.register_changeset(%User{}, params)
+ changeset = User.register_changeset(%User{info: %{}}, params)
with {:ok, user} <- Repo.insert(changeset) do
!registrations_open && UserInviteToken.mark_as_used(token.token)
@@ -279,14 +279,6 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
def get_external_profile(for_user, uri) do
with %User{} = user <- User.get_or_fetch(uri) do
- spawn(fn ->
- with url <- user.info["topic"],
- {:ok, %{body: body}} <-
- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
- OStatus.handle_incoming(body)
- end
- end)
-
{:ok, UserView.render("show.json", %{user: user, for: for_user})}
else
_e ->
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index cd0e2121c..0ccf937b0 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -4,7 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
- alias Pleroma.{Repo, Activity, User, Notification}
+ alias Pleroma.{Repo, Activity, Object, User, Notification}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Ecto.Changeset
@@ -226,16 +226,51 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
end
- def upload(conn, %{"media" => media}) do
- response = TwitterAPI.upload(media)
+ @doc """
+ Updates metadata of uploaded media object.
+ Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
+ """
+ def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
+ object = Repo.get(Object, id)
+ description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
+
+ {conn, status, response_body} =
+ cond do
+ !object ->
+ {halt(conn), :not_found, ""}
+
+ !Object.authorize_mutation(object, user) ->
+ {halt(conn), :forbidden, "You can only update your own uploads."}
+
+ !is_binary(description) ->
+ {conn, :not_modified, ""}
+
+ true ->
+ new_data = Map.put(object.data, "name", description)
+
+ {:ok, _} =
+ object
+ |> Object.change(%{data: new_data})
+ |> Repo.update()
+
+ {conn, :no_content, ""}
+ end
+
+ conn
+ |> put_status(status)
+ |> json(response_body)
+ end
+
+ def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
+ response = TwitterAPI.upload(media, user)
conn
|> put_resp_content_type("application/atom+xml")
|> send_resp(200, response)
end
- def upload_json(conn, %{"media" => media}) do
- response = TwitterAPI.upload(media, "json")
+ def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
+ response = TwitterAPI.upload(media, user, "json")
conn
|> json_reply(200, response)
@@ -290,11 +325,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def update_avatar(%{assigns: %{user: user}} = conn, params) do
- upload_limit =
- Application.get_env(:pleroma, :instance)
- |> Keyword.fetch(:avatar_upload_limit)
-
- {:ok, object} = ActivityPub.upload(params, upload_limit)
+ {:ok, object} = ActivityPub.upload(params, type: :avatar)
change = Changeset.change(user, %{avatar: object.data})
{:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user)
@@ -303,14 +334,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def update_banner(%{assigns: %{user: user}} = conn, params) do
- upload_limit =
- Application.get_env(:pleroma, :instance)
- |> Keyword.fetch(:banner_upload_limit)
-
- with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, upload_limit),
- new_info <- Map.put(user.info, "banner", object.data),
- change <- User.info_changeset(user, %{info: new_info}),
- {:ok, user} <- User.update_and_set_cache(change) do
+ with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
+ new_info <- %{"banner" => object.data},
+ info_cng <- User.Info.profile_update(user.info, new_info),
+ changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
+ {:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
%{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!()
@@ -321,14 +349,11 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
def update_background(%{assigns: %{user: user}} = conn, params) do
- upload_limit =
- Application.get_env(:pleroma, :instance)
- |> Keyword.fetch(:background_upload_limit)
-
- with {:ok, object} <- ActivityPub.upload(params, upload_limit),
- new_info <- Map.put(user.info, "background", object.data),
- change <- User.info_changeset(user, %{info: new_info}),
- {:ok, _user} <- User.update_and_set_cache(change) do
+ with {:ok, object} <- ActivityPub.upload(params, type: :background),
+ new_info <- %{"background" => object.data},
+ info_cng <- User.Info.profile_update(user.info, new_info),
+ changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
+ {:ok, _user} <- User.update_and_set_cache(changeset) do
%{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!()
@@ -350,32 +375,32 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
end
end
- def update_most_recent_notification(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with id when is_number(id) <- String.to_integer(id),
- info <- user.info,
- mrn <- max(id, user.info["most_recent_notification"] || 0),
- updated_info <- Map.put(info, "most_recent_notification", mrn),
- changeset <- User.info_changeset(user, %{info: updated_info}),
- {:ok, _user} <- User.update_and_set_cache(changeset) do
- conn
- |> json_reply(200, Jason.encode!(mrn))
- else
- _e -> bad_request_reply(conn, "Can't update.")
- end
- end
-
- def followers(conn, params) do
- with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
+ def followers(%{assigns: %{user: for_user}} = conn, params) do
+ with {:ok, user} <- TwitterAPI.get_user(for_user, params),
{:ok, followers} <- User.get_followers(user) do
+ followers =
+ cond do
+ for_user && user.id == for_user.id -> followers
+ user.info.hide_network -> []
+ true -> followers
+ end
+
render(conn, UserView, "index.json", %{users: followers, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get followers")
end
end
- def friends(conn, params) do
+ def friends(%{assigns: %{user: for_user}} = conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friends} <- User.get_friends(user) do
+ friends =
+ cond do
+ for_user && user.id == for_user.id -> friends
+ user.info.hide_network -> []
+ true -> friends
+ end
+
render(conn, UserView, "index.json", %{users: friends, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get friends")
@@ -451,67 +476,41 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
json(conn, [])
end
- def update_profile(%{assigns: %{user: user}} = conn, params) do
- params =
- if bio = params["description"] do
- mentions = Formatter.parse_mentions(bio)
- tags = Formatter.parse_tags(bio)
-
- emoji =
- (user.info["source_data"]["tag"] || [])
- |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
- |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
- {String.trim(name, ":"), url}
- end)
-
- bio_html = CommonUtils.format_input(bio, mentions, tags, "text/plain")
- Map.put(params, "bio", bio_html |> Formatter.emojify(emoji))
- else
- params
- end
-
- user =
- if locked = params["locked"] do
- with locked <- locked == "true",
- new_info <- Map.put(user.info, "locked", locked),
- change <- User.info_changeset(user, %{info: new_info}),
- {:ok, user} <- User.update_and_set_cache(change) do
- user
+ defp build_info_cng(user, params) do
+ info_params =
+ ["no_rich_text", "locked", "hide_network"]
+ |> Enum.reduce(%{}, fn key, res ->
+ if value = params[key] do
+ Map.put(res, key, value == "true")
else
- _e -> user
+ res
end
- else
- user
- end
+ end)
- user =
- if no_rich_text = params["no_rich_text"] do
- with no_rich_text <- no_rich_text == "true",
- new_info <- Map.put(user.info, "no_rich_text", no_rich_text),
- change <- User.info_changeset(user, %{info: new_info}),
- {:ok, user} <- User.update_and_set_cache(change) do
- user
- else
- _e -> user
- end
+ info_params =
+ if value = params["default_scope"] do
+ Map.put(info_params, "default_scope", value)
else
- user
+ info_params
end
- user =
- if default_scope = params["default_scope"] do
- with new_info <- Map.put(user.info, "default_scope", default_scope),
- change <- User.info_changeset(user, %{info: new_info}),
- {:ok, user} <- User.update_and_set_cache(change) do
- user
- else
- _e -> user
- end
- else
- user
- end
+ User.Info.profile_update(user.info, info_params)
+ end
+
+ defp parse_profile_bio(user, params) do
+ if bio = params["description"] do
+ Map.put(params, "bio", User.parse_bio(bio, user))
+ else
+ params
+ end
+ end
+
+ def update_profile(%{assigns: %{user: user}} = conn, params) do
+ params = parse_profile_bio(user, params)
+ info_cng = build_info_cng(user, params)
with changeset <- User.update_changeset(user, params),
+ changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
render(conn, UserView, "user.json", %{user: user, for: user})
diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex
index a100a1127..b78024ed7 100644
--- a/lib/pleroma/web/twitter_api/views/user_view.ex
+++ b/lib/pleroma/web/twitter_api/views/user_view.ex
@@ -31,7 +31,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
user_info = User.get_cached_user_info(user)
emoji =
- (user.info["source_data"]["tag"] || [])
+ (user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
@@ -40,7 +40,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
# For example: [{"name": "Pronoun", "value": "she/her"}, …]
fields =
- (user.info["source_data"]["attachment"] || [])
+ (user.info.source_data["attachment"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
@@ -66,17 +66,17 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"rights" => %{
- "delete_others_notice" => !!user.info["is_moderator"]
+ "delete_others_notice" => !!user.info.is_moderator
},
"screen_name" => user.nickname,
"statuses_count" => user_info[:note_count],
"statusnet_profile_url" => user.ap_id,
"cover_photo" => User.banner_url(user) |> MediaProxy.url(),
- "background_image" => image_url(user.info["background"]) |> MediaProxy.url(),
+ "background_image" => image_url(user.info.background) |> MediaProxy.url(),
"is_local" => user.local,
- "locked" => !!user.info["locked"],
- "default_scope" => user.info["default_scope"] || "public",
- "no_rich_text" => user.info["no_rich_text"] || false,
+ "locked" => user.info.locked,
+ "default_scope" => user.info.default_scope,
+ "no_rich_text" => user.info.no_rich_text,
"fields" => fields
}
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 9f554d286..99c65a6bf 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -45,7 +45,7 @@ defmodule Pleroma.Web.WebFinger do
def represent_user(user, "JSON") do
{:ok, user} = ensure_keys_present(user)
- {:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"])
+ {:ok, _private, public} = Salmon.keys_from_pem(user.info.keys)
magic_key = Salmon.encode_key(public)
%{
@@ -83,7 +83,7 @@ defmodule Pleroma.Web.WebFinger do
def represent_user(user, "XML") do
{:ok, user} = ensure_keys_present(user)
- {:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"])
+ {:ok, _private, public} = Salmon.keys_from_pem(user.info.keys)
magic_key = Salmon.encode_key(public)
{
@@ -113,16 +113,22 @@ defmodule Pleroma.Web.WebFinger do
# This seems a better fit in Salmon
def ensure_keys_present(user) do
- info = user.info || %{}
+ info = user.info
- if info["keys"] do
+ if info.keys do
{:ok, user}
else
{:ok, pem} = Salmon.generate_rsa_pem()
- info = Map.put(info, "keys", pem)
- Ecto.Changeset.change(user, info: info)
- |> User.update_and_set_cache()
+ info_cng =
+ info
+ |> Pleroma.User.Info.set_keys(pem)
+
+ cng =
+ Ecto.Changeset.change(user)
+ |> Ecto.Changeset.put_embed(:info, info_cng)
+
+ User.update_and_set_cache(cng)
end
end
@@ -214,7 +220,7 @@ defmodule Pleroma.Web.WebFinger do
end
def find_lrdd_template(domain) do
- with {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <-
+ with {:ok, %{status: status, body: body}} when status in 200..299 <-
@httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do
get_template_from_xml(body)
else
@@ -253,7 +259,7 @@ defmodule Pleroma.Web.WebFinger do
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
),
- {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- response do
+ {:ok, %{status: status, body: body}} when status in 200..299 <- response do
doc = XML.parse_document(body)
if doc != :error do
diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex
index 396dcf045..0761b5475 100644
--- a/lib/pleroma/web/websub/websub.ex
+++ b/lib/pleroma/web/websub/websub.ex
@@ -146,7 +146,7 @@ defmodule Pleroma.Web.Websub do
end
def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do
- topic = subscribed.info["topic"]
+ topic = subscribed.info.topic
# FIXME: Race condition, use transactions
{:ok, subscription} =
with subscription when not is_nil(subscription) <-
@@ -158,7 +158,7 @@ defmodule Pleroma.Web.Websub do
_e ->
subscription = %WebsubClientSubscription{
topic: topic,
- hub: subscribed.info["hub"],
+ hub: subscribed.info.hub,
subscribers: [subscriber.ap_id],
state: "requested",
secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64(),
@@ -173,7 +173,7 @@ defmodule Pleroma.Web.Websub do
def gather_feed_data(topic, getter \\ &@httpoison.get/1) do
with {:ok, response} <- getter.(topic),
- status_code when status_code in 200..299 <- response.status_code,
+ status when status in 200..299 <- response.status,
body <- response.body,
doc <- XML.parse_document(body),
uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc),
@@ -221,7 +221,7 @@ defmodule Pleroma.Web.Websub do
task = Task.async(websub_checker)
- with {:ok, %{status_code: 202}} <-
+ with {:ok, %{status: 202}} <-
poster.(websub.hub, {:form, data}, "Content-type": "application/x-www-form-urlencoded"),
{:ok, websub} <- Task.yield(task, timeout) do
{:ok, websub}
@@ -257,7 +257,7 @@ defmodule Pleroma.Web.Websub do
signature = sign(secret || "", xml)
Logger.info(fn -> "Pushing #{topic} to #{callback}" end)
- with {:ok, %{status_code: code}} <-
+ with {:ok, %{status: code}} <-
@httpoison.post(
callback,
xml,
@@ -265,9 +265,11 @@ defmodule Pleroma.Web.Websub do
{"Content-Type", "application/atom+xml"},
{"X-Hub-Signature", "sha1=#{signature}"}
],
- timeout: 10000,
- recv_timeout: 20000,
- hackney: [pool: :default]
+ adapter: [
+ timeout: 10000,
+ recv_timeout: 20000,
+ pool: :default
+ ]
) do
Logger.info(fn -> "Pushed to #{callback}, code #{code}" end)
{:ok, code}