From f8786fa6f27b1934b48b69fce5d285ebddefda92 Mon Sep 17 00:00:00 2001 From: Alex S Date: Wed, 10 Jul 2019 16:01:32 +0300 Subject: adding following_address field to user --- lib/pleroma/user.ex | 30 ++++++++++++++++++++-------- lib/pleroma/web/activity_pub/activity_pub.ex | 1 + 2 files changed, 23 insertions(+), 8 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 034c414bf..81efb4f13 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -52,6 +52,7 @@ defmodule Pleroma.User do field(:avatar, :map) field(:local, :boolean, default: true) field(:follower_address, :string) + field(:following_address, :string) field(:search_rank, :float, virtual: true) field(:search_type, :integer, virtual: true) field(:tags, {:array, :string}, default: []) @@ -162,9 +163,10 @@ defmodule Pleroma.User do if changes.valid? do case info_cng.changes[:source_data] do - %{"followers" => followers} -> + %{"followers" => followers, "following" => following} -> changes |> put_change(:follower_address, followers) + |> put_change(:following_address, following) _ -> followers = User.ap_followers(%User{nickname: changes.changes[:nickname]}) @@ -196,7 +198,14 @@ defmodule Pleroma.User do |> User.Info.user_upgrade(params[:info]) struct - |> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at]) + |> cast(params, [ + :bio, + :name, + :follower_address, + :following_address, + :avatar, + :last_refreshed_at + ]) |> unique_constraint(:nickname) |> validate_format(:nickname, local_nickname_regex()) |> validate_length(:bio, max: 5000) @@ -1039,15 +1048,20 @@ defmodule Pleroma.User do end end + @spec external_users_query() :: Ecto.Query.t() + def external_users_query do + User.Query.build(%{ + external: true, + active: true, + order_by: :id + }) + end + @spec external_users(keyword()) :: [User.t()] def external_users(opts \\ []) do query = - User.Query.build(%{ - external: true, - active: true, - order_by: :id, - select: [:id, :ap_id, :info] - }) + external_users_query() + |> select([u], struct(u, [:id, :ap_id, :info])) query = if opts[:max_id], diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 41b55bbab..a3174a787 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -994,6 +994,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do avatar: avatar, name: data["name"], follower_address: data["followers"], + following_address: data["following"], bio: data["summary"] } -- cgit v1.2.3 From beba7bbc8550aca07874e105b784b7a3cbe89838 Mon Sep 17 00:00:00 2001 From: Alex S Date: Wed, 10 Jul 2019 17:39:07 +0300 Subject: removing synchronization worker --- lib/pleroma/application.ex | 6 +-- lib/pleroma/user.ex | 32 +++----------- lib/pleroma/user/synchronization.ex | 60 -------------------------- lib/pleroma/user/synchronization_worker.ex | 32 -------------- lib/pleroma/web/activity_pub/transmogrifier.ex | 27 ++++++++++++ 5 files changed, 33 insertions(+), 124 deletions(-) delete mode 100644 lib/pleroma/user/synchronization.ex delete mode 100644 lib/pleroma/user/synchronization_worker.ex (limited to 'lib') diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 86c348a0d..ba4cf8486 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -151,11 +151,7 @@ defmodule Pleroma.Application do start: {Pleroma.Web.Endpoint, :start_link, []}, type: :supervisor }, - %{id: Pleroma.Gopher.Server, start: {Pleroma.Gopher.Server, :start_link, []}}, - %{ - id: Pleroma.User.SynchronizationWorker, - start: {Pleroma.User.SynchronizationWorker, :start_link, []} - } + %{id: Pleroma.Gopher.Server, start: {Pleroma.Gopher.Server, :start_link, []}} ] # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 81efb4f13..e5a6c2529 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -108,6 +108,10 @@ defmodule Pleroma.User do def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers" + @spec ap_following(User.t()) :: Sring.t() + def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa + def ap_following(%User{} = user), do: "#{ap_id(user)}/following" + def user_info(%User{} = user, args \\ %{}) do following_count = if args[:following_count], do: args[:following_count], else: following_count(user) @@ -129,6 +133,7 @@ defmodule Pleroma.User do Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args)) end + @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t() def restrict_deactivated(query) do from(u in query, where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info) @@ -1021,33 +1026,6 @@ defmodule Pleroma.User do ) end - @spec sync_follow_counter() :: :ok - def sync_follow_counter, - do: PleromaJobQueue.enqueue(:background, __MODULE__, [:sync_follow_counters]) - - @spec perform(:sync_follow_counters) :: :ok - def perform(:sync_follow_counters) do - {:ok, _pid} = Agent.start_link(fn -> %{} end, name: :domain_errors) - config = Pleroma.Config.get([:instance, :external_user_synchronization]) - - :ok = sync_follow_counters(config) - Agent.stop(:domain_errors) - end - - @spec sync_follow_counters(keyword()) :: :ok - def sync_follow_counters(opts \\ []) do - users = external_users(opts) - - if length(users) > 0 do - errors = Agent.get(:domain_errors, fn state -> state end) - {last, updated_errors} = User.Synchronization.call(users, errors, opts) - Agent.update(:domain_errors, fn _state -> updated_errors end) - sync_follow_counters(max_id: last.id, limit: opts[:limit]) - else - :ok - end - end - @spec external_users_query() :: Ecto.Query.t() def external_users_query do User.Query.build(%{ diff --git a/lib/pleroma/user/synchronization.ex b/lib/pleroma/user/synchronization.ex deleted file mode 100644 index 93660e08c..000000000 --- a/lib/pleroma/user/synchronization.ex +++ /dev/null @@ -1,60 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.User.Synchronization do - alias Pleroma.HTTP - alias Pleroma.User - - @spec call([User.t()], map(), keyword()) :: {User.t(), map()} - def call(users, errors, opts \\ []) do - do_call(users, errors, opts) - end - - defp do_call([user | []], errors, opts) do - updated = fetch_counters(user, errors, opts) - {user, updated} - end - - defp do_call([user | others], errors, opts) do - updated = fetch_counters(user, errors, opts) - do_call(others, updated, opts) - end - - defp fetch_counters(user, errors, opts) do - %{host: host} = URI.parse(user.ap_id) - - info = %{} - {following, errors} = fetch_counter(user.ap_id <> "/following", host, errors, opts) - info = if following, do: Map.put(info, :following_count, following), else: info - - {followers, errors} = fetch_counter(user.ap_id <> "/followers", host, errors, opts) - info = if followers, do: Map.put(info, :follower_count, followers), else: info - - User.set_info_cache(user, info) - errors - end - - defp available_domain?(domain, errors, opts) do - max_retries = Keyword.get(opts, :max_retries, 3) - not (Map.has_key?(errors, domain) && errors[domain] >= max_retries) - end - - defp fetch_counter(url, host, errors, opts) do - with true <- available_domain?(host, errors, opts), - {:ok, %{body: body, status: code}} when code in 200..299 <- - HTTP.get( - url, - [{:Accept, "application/activity+json"}] - ), - {:ok, data} <- Jason.decode(body) do - {data["totalItems"], errors} - else - false -> - {nil, errors} - - _ -> - {nil, Map.update(errors, host, 1, &(&1 + 1))} - end - end -end diff --git a/lib/pleroma/user/synchronization_worker.ex b/lib/pleroma/user/synchronization_worker.ex deleted file mode 100644 index ba9cc3556..000000000 --- a/lib/pleroma/user/synchronization_worker.ex +++ /dev/null @@ -1,32 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2018 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-onl - -defmodule Pleroma.User.SynchronizationWorker do - use GenServer - - def start_link do - config = Pleroma.Config.get([:instance, :external_user_synchronization]) - - if config[:enabled] do - GenServer.start_link(__MODULE__, interval: config[:interval]) - else - :ignore - end - end - - def init(opts) do - schedule_next(opts) - {:ok, opts} - end - - def handle_info(:sync_follow_counters, opts) do - Pleroma.User.sync_follow_counter() - schedule_next(opts) - {:noreply, opts} - end - - defp schedule_next(opts) do - Process.send_after(self(), :sync_follow_counters, opts[:interval]) - end -end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index e34fe6611..d14490bb5 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -1087,6 +1087,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user]) end + if Pleroma.Config.get([:instance, :external_user_synchronization]) do + update_following_followers_counters(user) + end + {:ok, user} else %User{} = user -> {:ok, user} @@ -1119,4 +1123,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do data |> maybe_fix_user_url end + + def update_following_followers_counters(user) do + info = %{} + + following = fetch_counter(user.following_address) + info = if following, do: Map.put(info, :following_count, following), else: info + + followers = fetch_counter(user.follower_address) + info = if followers, do: Map.put(info, :follower_count, followers), else: info + + User.set_info_cache(user, info) + end + + defp fetch_counter(url) do + with {:ok, %{body: body, status: code}} when code in 200..299 <- + Pleroma.HTTP.get( + url, + [{:Accept, "application/activity+json"}] + ), + {:ok, data} <- Jason.decode(body) do + data["totalItems"] + end + end end -- cgit v1.2.3 From 846ad9a463e7d6767170305f32eef7bbd09f8a6b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 11 Jul 2019 13:02:13 +0000 Subject: admin api configure changes --- lib/pleroma/web/admin_api/admin_api_controller.ex | 8 +- lib/pleroma/web/admin_api/config.ex | 105 ++++++++++------------ lib/pleroma/web/admin_api/views/config_view.ex | 2 +- 3 files changed, 51 insertions(+), 64 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 8b3c3c91f..4a0bf4823 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -371,13 +371,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do if Pleroma.Config.get([:instance, :dynamic_configuration]) do updated = Enum.map(configs, fn - %{"group" => group, "key" => key, "value" => value} -> - {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value}) - config - %{"group" => group, "key" => key, "delete" => "true"} -> {:ok, _} = Config.delete(%{group: group, key: key}) nil + + %{"group" => group, "key" => key, "value" => value} -> + {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value}) + config end) |> Enum.reject(&is_nil(&1)) diff --git a/lib/pleroma/web/admin_api/config.ex b/lib/pleroma/web/admin_api/config.ex index 24674abc5..b4eb8e002 100644 --- a/lib/pleroma/web/admin_api/config.ex +++ b/lib/pleroma/web/admin_api/config.ex @@ -67,99 +67,86 @@ defmodule Pleroma.Web.AdminAPI.Config do end @spec from_binary(binary()) :: term() - def from_binary(value), do: :erlang.binary_to_term(value) + def from_binary(binary), do: :erlang.binary_to_term(binary) - @spec from_binary_to_map(binary()) :: any() - def from_binary_to_map(binary) do + @spec from_binary_with_convert(binary()) :: any() + def from_binary_with_convert(binary) do from_binary(binary) |> do_convert() end - defp do_convert([{k, v}] = value) when is_list(value) and length(value) == 1, - do: %{k => do_convert(v)} + defp do_convert(entity) when is_list(entity) do + for v <- entity, into: [], do: do_convert(v) + end - defp do_convert(values) when is_list(values), do: for(val <- values, do: do_convert(val)) + defp do_convert(entity) when is_map(entity) do + for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)} + end - defp do_convert({k, v} = value) when is_tuple(value), - do: %{k => do_convert(v)} + defp do_convert({:dispatch, [entity]}), do: %{"tuple" => [":dispatch", [inspect(entity)]]} - defp do_convert(value) when is_tuple(value), do: %{"tuple" => do_convert(Tuple.to_list(value))} + defp do_convert(entity) when is_tuple(entity), + do: %{"tuple" => do_convert(Tuple.to_list(entity))} - defp do_convert(value) when is_binary(value) or is_map(value) or is_number(value), do: value + defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity), + do: entity - defp do_convert(value) when is_atom(value) do - string = to_string(value) + defp do_convert(entity) when is_atom(entity) do + string = to_string(entity) if String.starts_with?(string, "Elixir."), - do: String.trim_leading(string, "Elixir."), - else: value + do: do_convert(string), + else: ":" <> string end - @spec transform(any()) :: binary() - def transform(%{"tuple" => _} = entity), do: :erlang.term_to_binary(do_transform(entity)) - - def transform(entity) when is_map(entity) do - tuples = - for {k, v} <- entity, - into: [], - do: {if(is_atom(k), do: k, else: String.to_atom(k)), do_transform(v)} + defp do_convert("Elixir." <> module_name), do: module_name - Enum.reject(tuples, fn {_k, v} -> is_nil(v) end) - |> Enum.sort() - |> :erlang.term_to_binary() - end + defp do_convert(entity) when is_binary(entity), do: entity - def transform(entity) when is_list(entity) do - list = Enum.map(entity, &do_transform(&1)) - :erlang.term_to_binary(list) + @spec transform(any()) :: binary() + def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do + :erlang.term_to_binary(do_transform(entity)) end def transform(entity), do: :erlang.term_to_binary(entity) - defp do_transform(%Regex{} = value) when is_map(value), do: value + defp do_transform(%Regex{} = entity) when is_map(entity), do: entity - defp do_transform(%{"tuple" => [k, values] = entity}) when length(entity) == 2 do - {do_transform(k), do_transform(values)} + defp do_transform(%{"tuple" => [":dispatch", [entity]]}) do + cleaned_string = String.replace(entity, ~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "") + {dispatch_settings, []} = Code.eval_string(cleaned_string, [], requires: [], macros: []) + {:dispatch, [dispatch_settings]} end - defp do_transform(%{"tuple" => values}) do - Enum.reduce(values, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end) + defp do_transform(%{"tuple" => entity}) do + Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end) end - defp do_transform(value) when is_map(value) do - values = for {key, val} <- value, into: [], do: {String.to_atom(key), do_transform(val)} - - Enum.sort(values) + defp do_transform(entity) when is_map(entity) do + for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)} end - defp do_transform(value) when is_list(value) do - Enum.map(value, &do_transform(&1)) + defp do_transform(entity) when is_list(entity) do + for v <- entity, into: [], do: do_transform(v) end - defp do_transform(entity) when is_list(entity) and length(entity) == 1, do: hd(entity) - - defp do_transform(value) when is_binary(value) do - String.trim(value) + defp do_transform(entity) when is_binary(entity) do + String.trim(entity) |> do_transform_string() end - defp do_transform(value), do: value - - defp do_transform_string(value) when byte_size(value) == 0, do: nil - - defp do_transform_string(value) do - cond do - String.starts_with?(value, "Pleroma") or String.starts_with?(value, "Phoenix") -> - String.to_existing_atom("Elixir." <> value) + defp do_transform(entity), do: entity - String.starts_with?(value, ":") -> - String.replace(value, ":", "") |> String.to_existing_atom() + defp do_transform_string("~r/" <> pattern) do + pattern = String.trim_trailing(pattern, "/") + ~r/#{pattern}/ + end - String.starts_with?(value, "i:") -> - String.replace(value, "i:", "") |> String.to_integer() + defp do_transform_string(":" <> atom), do: String.to_atom(atom) - true -> - value - end + defp do_transform_string(value) do + if String.starts_with?(value, "Pleroma") or String.starts_with?(value, "Phoenix"), + do: String.to_existing_atom("Elixir." <> value), + else: value end end diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex index a31f1041f..49add0b6e 100644 --- a/lib/pleroma/web/admin_api/views/config_view.ex +++ b/lib/pleroma/web/admin_api/views/config_view.ex @@ -15,7 +15,7 @@ defmodule Pleroma.Web.AdminAPI.ConfigView do %{ key: config.key, group: config.group, - value: Pleroma.Web.AdminAPI.Config.from_binary_to_map(config.value) + value: Pleroma.Web.AdminAPI.Config.from_binary_with_convert(config.value) } end end -- cgit v1.2.3 From 4198c3ac390edaab04a61a179b1f8bc5adaf89de Mon Sep 17 00:00:00 2001 From: Eugenij Date: Thu, 11 Jul 2019 13:55:31 +0000 Subject: Extend Pleroma.Pagination to support offset-based pagination, use async/await to execute status and account search in parallel --- lib/pleroma/activity.ex | 2 +- lib/pleroma/activity/search.ex | 21 +++- lib/pleroma/pagination.ex | 29 +++++- lib/pleroma/user/search.ex | 8 +- lib/pleroma/web/mastodon_api/search_controller.ex | 116 ++++++++++++++-------- 5 files changed, 118 insertions(+), 58 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 6db41fe6e..46552c7be 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -344,5 +344,5 @@ defmodule Pleroma.Activity do ) end - defdelegate search(user, query), to: Pleroma.Activity.Search + defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search end diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index 0aa2aab23..0cc3770a7 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -5,14 +5,17 @@ defmodule Pleroma.Activity.Search do alias Pleroma.Activity alias Pleroma.Object.Fetcher - alias Pleroma.Repo + alias Pleroma.Pagination alias Pleroma.User alias Pleroma.Web.ActivityPub.Visibility import Ecto.Query - def search(user, search_query) do + def search(user, search_query, options \\ []) do index_type = if Pleroma.Config.get([:database, :rum_enabled]), do: :rum, else: :gin + limit = Enum.min([Keyword.get(options, :limit), 40]) + offset = Keyword.get(options, :offset, 0) + author = Keyword.get(options, :author) Activity |> Activity.with_preloaded_object() @@ -20,15 +23,23 @@ defmodule Pleroma.Activity.Search do |> restrict_public() |> query_with(index_type, search_query) |> maybe_restrict_local(user) - |> Repo.all() + |> maybe_restrict_author(author) + |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => limit}, :offset) |> maybe_fetch(user, search_query) end + def maybe_restrict_author(query, %User{} = author) do + from([a, o] in query, + where: a.actor == ^author.ap_id + ) + end + + def maybe_restrict_author(query, _), do: query + defp restrict_public(q) do from([a, o] in q, where: fragment("?->>'type' = 'Create'", a.data), - where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients, - limit: 40 + where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients ) end diff --git a/lib/pleroma/pagination.ex b/lib/pleroma/pagination.ex index 3d7dd9e6a..2b869ccdc 100644 --- a/lib/pleroma/pagination.ex +++ b/lib/pleroma/pagination.ex @@ -14,16 +14,28 @@ defmodule Pleroma.Pagination do @default_limit 20 - def fetch_paginated(query, params) do + def fetch_paginated(query, params, type \\ :keyset) + + def fetch_paginated(query, params, :keyset) do options = cast_params(params) query - |> paginate(options) + |> paginate(options, :keyset) |> Repo.all() |> enforce_order(options) end - def paginate(query, options) do + def fetch_paginated(query, params, :offset) do + options = cast_params(params) + + query + |> paginate(options, :offset) + |> Repo.all() + end + + def paginate(query, options, method \\ :keyset) + + def paginate(query, options, :keyset) do query |> restrict(:min_id, options) |> restrict(:since_id, options) @@ -32,11 +44,18 @@ defmodule Pleroma.Pagination do |> restrict(:limit, options) end + def paginate(query, options, :offset) do + query + |> restrict(:offset, options) + |> restrict(:limit, options) + end + defp cast_params(params) do param_types = %{ min_id: :string, since_id: :string, max_id: :string, + offset: :integer, limit: :integer } @@ -70,6 +89,10 @@ defmodule Pleroma.Pagination do order_by(query, [u], fragment("? desc nulls last", u.id)) end + defp restrict(query, :offset, %{offset: offset}) do + offset(query, ^offset) + end + defp restrict(query, :limit, options) do limit = Map.get(options, :limit, @default_limit) diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index e0fc6daa6..46620b89a 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.User.Search do + alias Pleroma.Pagination alias Pleroma.Repo alias Pleroma.User import Ecto.Query @@ -32,8 +33,7 @@ defmodule Pleroma.User.Search do query_string |> search_query(for_user, following) - |> paginate(result_limit, offset) - |> Repo.all() + |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset) end) results @@ -87,10 +87,6 @@ defmodule Pleroma.User.Search do defp filter_blocked_domains(query, _), do: query - defp paginate(query, limit, offset) do - from(q in query, limit: ^limit, offset: ^offset) - end - defp union_subqueries({fts_subquery, trigram_subquery}) do from(s in trigram_subquery, union_all: ^fts_subquery) end diff --git a/lib/pleroma/web/mastodon_api/search_controller.ex b/lib/pleroma/web/mastodon_api/search_controller.ex index 939f7f6cb..9072aa7a4 100644 --- a/lib/pleroma/web/mastodon_api/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/search_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do alias Pleroma.Activity alias Pleroma.Plugs.RateLimiter + alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web alias Pleroma.Web.ControllerHelper @@ -16,43 +17,6 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do require Logger plug(RateLimiter, :search when action in [:search, :search2, :account_search]) - def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = with_fallback(fn -> User.search(query, search_options(params, user)) end, []) - statuses = with_fallback(fn -> Activity.search(user, query) end, []) - - tags_path = Web.base_url() <> "/tag/" - - tags = - query - |> prepare_tags - |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) - - res = %{ - "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), - "statuses" => - StatusView.render("index.json", activities: statuses, for: user, as: :activity), - "hashtags" => tags - } - - json(conn, res) - end - - def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = with_fallback(fn -> User.search(query, search_options(params, user)) end) - statuses = with_fallback(fn -> Activity.search(user, query) end) - - tags = prepare_tags(query) - - res = %{ - "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), - "statuses" => - StatusView.render("index.json", activities: statuses, for: user, as: :activity), - "hashtags" => tags - } - - json(conn, res) - end - def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do accounts = User.search(query, search_options(params, user)) res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) @@ -60,12 +24,36 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do json(conn, res) end - defp prepare_tags(query) do - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) - |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) + def search2(conn, params), do: do_search(:v2, conn, params) + def search(conn, params), do: do_search(:v1, conn, params) + + defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do + options = search_options(params, user) + timeout = Keyword.get(Repo.config(), :timeout, 15_000) + default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} + + result = + default_values + |> Enum.map(fn {resource, default_value} -> + if params["type"] == nil or params["type"] == resource do + {resource, fn -> resource_search(version, resource, query, options) end} + else + {resource, fn -> default_value end} + end + end) + |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end, + timeout: timeout, + on_timeout: :kill_task + ) + |> Enum.reduce(default_values, fn + {:ok, {resource, result}}, acc -> + Map.put(acc, resource, result) + + _error, acc -> + acc + end) + + json(conn, result) end defp search_options(params, user) do @@ -74,8 +62,45 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do following: params["following"] == "true", limit: ControllerHelper.fetch_integer_param(params, "limit"), offset: ControllerHelper.fetch_integer_param(params, "offset"), + type: params["type"], + author: get_author(params), for_user: user ] + |> Enum.filter(&elem(&1, 1)) + end + + defp resource_search(_, "accounts", query, options) do + accounts = with_fallback(fn -> User.search(query, options) end) + AccountView.render("accounts.json", users: accounts, for: options[:for_user], as: :user) + end + + defp resource_search(_, "statuses", query, options) do + statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end) + StatusView.render("index.json", activities: statuses, for: options[:for_user], as: :activity) + end + + defp resource_search(:v2, "hashtags", query, _options) do + tags_path = Web.base_url() <> "/tag/" + + query + |> prepare_tags() + |> Enum.map(fn tag -> + tag = String.trim_leading(tag, "#") + %{name: tag, url: tags_path <> tag} + end) + end + + defp resource_search(:v1, "hashtags", query, _options) do + query + |> prepare_tags() + |> Enum.map(fn tag -> String.trim_leading(tag, "#") end) + end + + defp prepare_tags(query) do + query + |> String.split() + |> Enum.uniq() + |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) end defp with_fallback(f, fallback \\ []) do @@ -87,4 +112,9 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do fallback end end + + defp get_author(%{"account_id" => account_id}) when is_binary(account_id), + do: User.get_cached_by_id(account_id) + + defp get_author(_params), do: nil end -- cgit v1.2.3 From 6a6c4d134b7564012d00e89f1236b904261ab5db Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Fri, 12 Jul 2019 21:02:55 +0545 Subject: preserve the original path/filename (no encoding/decoding) for proxy --- lib/pleroma/web/media_proxy/controller.ex | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/controller.ex index c0552d89f..ea33d7685 100644 --- a/lib/pleroma/web/media_proxy/controller.ex +++ b/lib/pleroma/web/media_proxy/controller.ex @@ -28,12 +28,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do end def filename_matches(has_filename, path, url) do - filename = - url - |> MediaProxy.filename() - |> URI.decode() - - path = URI.decode(path) + filename = url |> MediaProxy.filename() if has_filename && filename && Path.basename(path) != filename do {:wrong_filename, filename} -- cgit v1.2.3 From 360e4cdaa2708d54903765c61afbc5ea5f1b2cdb Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 12 Jul 2019 11:25:58 -0500 Subject: Move these to pleroma namespace in Mastodon API --- lib/pleroma/web/router.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index d53fa8a35..a3b6ea366 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -322,10 +322,6 @@ defmodule Pleroma.Web.Router do patch("/accounts/update_credentials", MastodonAPIController, :update_credentials) - patch("/accounts/update_avatar", MastodonAPIController, :update_avatar) - patch("/accounts/update_banner", MastodonAPIController, :update_banner) - patch("/accounts/update_background", MastodonAPIController, :update_background) - post("/statuses", MastodonAPIController, :post_status) delete("/statuses/:id", MastodonAPIController, :delete_status) @@ -360,6 +356,10 @@ defmodule Pleroma.Web.Router do put("/filters/:id", MastodonAPIController, :update_filter) delete("/filters/:id", MastodonAPIController, :delete_filter) + patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar) + patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner) + patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background) + get("/pleroma/mascot", MastodonAPIController, :get_mascot) put("/pleroma/mascot", MastodonAPIController, :set_mascot) -- cgit v1.2.3 From 1f6ac7680d1ae07be7c7dfd81a8cec2ba52f1c82 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 12 Jul 2019 19:41:05 +0300 Subject: ActivityPub User view: Following/Followers refactoring - Render the collection items if the user requesting == the user rendered - Do not render the first page if hide_{followers,follows} is set, just give the URI to it --- lib/pleroma/web/activity_pub/views/user_view.ex | 39 +++++++++++++++++-------- 1 file changed, 27 insertions(+), 12 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 327e0e05b..d9c1bcb2c 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -98,29 +98,31 @@ defmodule Pleroma.Web.ActivityPub.UserView do |> Map.merge(Utils.make_json_ld_header()) end - def render("following.json", %{user: user, page: page}) do + def render("following.json", %{user: user, page: page} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows query = User.get_friends_query(user) query = from(user in query, select: [:ap_id]) following = Repo.all(query) total = - if !user.info.hide_follows do + if showing do length(following) else 0 end - collection(following, "#{user.ap_id}/following", page, !user.info.hide_follows, total) + collection(following, "#{user.ap_id}/following", page, showing, total) |> Map.merge(Utils.make_json_ld_header()) end - def render("following.json", %{user: user}) do + def render("following.json", %{user: user} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows query = User.get_friends_query(user) query = from(user in query, select: [:ap_id]) following = Repo.all(query) total = - if !user.info.hide_follows do + if showing do length(following) else 0 @@ -130,34 +132,43 @@ defmodule Pleroma.Web.ActivityPub.UserView do "id" => "#{user.ap_id}/following", "type" => "OrderedCollection", "totalItems" => total, - "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows) + "first" => + if showing do + collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows) + else + "#{user.ap_id}/following?page=1" + end } |> Map.merge(Utils.make_json_ld_header()) end - def render("followers.json", %{user: user, page: page}) do + def render("followers.json", %{user: user, page: page} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers + query = User.get_followers_query(user) query = from(user in query, select: [:ap_id]) followers = Repo.all(query) total = - if !user.info.hide_followers do + if showing do length(followers) else 0 end - collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_followers, total) + collection(followers, "#{user.ap_id}/followers", page, showing, total) |> Map.merge(Utils.make_json_ld_header()) end - def render("followers.json", %{user: user}) do + def render("followers.json", %{user: user} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers + query = User.get_followers_query(user) query = from(user in query, select: [:ap_id]) followers = Repo.all(query) total = - if !user.info.hide_followers do + if showing do length(followers) else 0 @@ -168,7 +179,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do "type" => "OrderedCollection", "totalItems" => total, "first" => - collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_followers, total) + if showing do + collection(followers, "#{user.ap_id}/followers", 1, showing, total) + else + "#{user.ap_id}/followers?page=1" + end } |> Map.merge(Utils.make_json_ld_header()) end -- cgit v1.2.3 From 92055941bd55cb75ab7b5a26d03918390e64b754 Mon Sep 17 00:00:00 2001 From: Maksim Date: Fri, 12 Jul 2019 16:42:54 +0000 Subject: Pleroma.Web.Metadata - tests --- lib/pleroma/web/metadata/opengraph.ex | 17 ++++-------- lib/pleroma/web/metadata/twitter_card.ex | 47 ++++++++++++-------------------- lib/pleroma/web/metadata/utils.ex | 7 +++++ 3 files changed, 29 insertions(+), 42 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex index 4033ec38f..e7fa7f408 100644 --- a/lib/pleroma/web/metadata/opengraph.ex +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do alias Pleroma.Web.Metadata.Utils @behaviour Provider + @media_types ["image", "audio", "video"] @impl Provider def build_tags(%{ @@ -81,26 +82,19 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = Enum.reduce(attachment["url"], [], fn url, acc -> - media_type = - Enum.find(["image", "audio", "video"], fn media_type -> - String.starts_with?(url["mediaType"], media_type) - end) - # TODO: Add additional properties to objects when we have the data available. # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. - case media_type do + case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> [ - {:meta, - [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} + {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} | acc ] "image" -> [ - {:meta, - [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}, + {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, {:meta, [property: "og:image:width", content: 150], []}, {:meta, [property: "og:image:height", content: 150], []} | acc @@ -108,8 +102,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do "video" -> [ - {:meta, - [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} + {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} | acc ] diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index 8dd01e0d5..d6a6049b3 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -1,4 +1,5 @@ # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors # SPDX-License-Identifier: AGPL-3.0-only @@ -9,13 +10,10 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do alias Pleroma.Web.Metadata.Utils @behaviour Provider + @media_types ["image", "audio", "video"] @impl Provider - def build_tags(%{ - activity_id: id, - object: object, - user: user - }) do + def build_tags(%{activity_id: id, object: object, user: user}) do attachments = build_attachments(id, object) scrubbed_content = Utils.scrub_html_and_truncate(object) # Zero width space @@ -27,21 +25,12 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do end [ - {:meta, - [ - property: "twitter:title", - content: Utils.user_name_string(user) - ], []}, - {:meta, - [ - property: "twitter:description", - content: content - ], []} + title_tag(user), + {:meta, [property: "twitter:description", content: content], []} ] ++ if attachments == [] or Metadata.activity_nsfw?(object) do [ - {:meta, - [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []}, + image_tag(user), {:meta, [property: "twitter:card", content: "summary_large_image"], []} ] else @@ -53,30 +42,28 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do def build_tags(%{user: user}) do with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do [ - {:meta, - [ - property: "twitter:title", - content: Utils.user_name_string(user) - ], []}, + title_tag(user), {:meta, [property: "twitter:description", content: truncated_bio], []}, - {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], - []}, + image_tag(user), {:meta, [property: "twitter:card", content: "summary"], []} ] end end + defp title_tag(user) do + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} + end + + def image_tag(user) do + {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} + end + defp build_attachments(id, %{data: %{"attachment" => attachments}}) do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = Enum.reduce(attachment["url"], [], fn url, acc -> - media_type = - Enum.find(["image", "audio", "video"], fn media_type -> - String.starts_with?(url["mediaType"], media_type) - end) - # TODO: Add additional properties to objects when we have the data available. - case media_type do + case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> [ {:meta, [property: "twitter:card", content: "player"], []}, diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index 58385a3d1..720bd4519 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -39,4 +39,11 @@ defmodule Pleroma.Web.Metadata.Utils do "(@#{user.nickname})" end end + + @spec fetch_media_type(list(String.t()), String.t()) :: String.t() | nil + def fetch_media_type(supported_types, media_type) do + Enum.find(supported_types, fn support_type -> + String.starts_with?(media_type, support_type) + end) + end end -- cgit v1.2.3 From 97b79efbcd4ad829a575019f842e7dcd7548266a Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 12 Jul 2019 20:54:20 +0300 Subject: ActivityPub Controller: Actually pass for_user to following/followers views and give 403 errors when trying to request hidden follower pages when unauthenticated --- .../web/activity_pub/activity_pub_controller.ex | 51 +++++++++++++++++----- lib/pleroma/web/router.ex | 8 +++- 2 files changed, 45 insertions(+), 14 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index cf5176201..e2af4ad1a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -103,43 +103,57 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end - def following(conn, %{"nickname" => nickname, "page" => page}) do + def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), + {:show_follows, true} <- + {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do {page, _} = Integer.parse(page) conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("following.json", %{user: user, page: page})) + |> json(UserView.render("following.json", %{user: user, page: page, for: for_user})) + else + {:show_follows, _} -> + conn + |> put_resp_header("content-type", "application/activity+json") + |> send_resp(403, "") end end - def following(conn, %{"nickname" => nickname}) do + def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("following.json", %{user: user})) + |> json(UserView.render("following.json", %{user: user, for: for_user})) end end - def followers(conn, %{"nickname" => nickname, "page" => page}) do + def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), + {:show_followers, true} <- + {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do {page, _} = Integer.parse(page) conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("followers.json", %{user: user, page: page})) + |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user})) + else + {:show_followers, _} -> + conn + |> put_resp_header("content-type", "application/activity+json") + |> send_resp(403, "") end end - def followers(conn, %{"nickname" => nickname}) do + def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("followers.json", %{user: user})) + |> json(UserView.render("followers.json", %{user: user, for: for_user})) end end @@ -325,4 +339,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do conn end + + defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do + {:ok, new_user} = User.ensure_keys_present(user) + + for_user = + if new_user != user and match?(%User{}, for_user) do + User.get_cached_by_nickname(for_user.nickname) + else + for_user + end + + {new_user, for_user} + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index d53fa8a35..e03a3a2e5 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -623,8 +623,6 @@ defmodule Pleroma.Web.Router do # XXX: not really ostatus pipe_through(:ostatus) - get("/users/:nickname/followers", ActivityPubController, :followers) - get("/users/:nickname/following", ActivityPubController, :following) get("/users/:nickname/outbox", ActivityPubController, :outbox) get("/objects/:uuid/likes", ActivityPubController, :object_likes) end @@ -656,6 +654,12 @@ defmodule Pleroma.Web.Router do pipe_through(:oauth_write) post("/users/:nickname/outbox", ActivityPubController, :update_outbox) end + + scope [] do + pipe_through(:oauth_read_or_public) + get("/users/:nickname/followers", ActivityPubController, :followers) + get("/users/:nickname/following", ActivityPubController, :following) + end end scope "/relay", Pleroma.Web.ActivityPub do -- cgit v1.2.3 From b001b8891a0ae9d8c7291f8148eb68a354cd319f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 12 Jul 2019 23:52:26 +0300 Subject: Merge the default options with custom ones in ReverseProxy and Pleroma.HTTP --- lib/pleroma/http/connection.ex | 2 +- lib/pleroma/http/http.ex | 5 +---- lib/pleroma/reverse_proxy/reverse_proxy.ex | 5 +++-- 3 files changed, 5 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index c216cdcb1..a1460d303 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -29,7 +29,7 @@ defmodule Pleroma.HTTP.Connection do # fetch Hackney options # - defp hackney_options(opts) do + def hackney_options(opts) do options = Keyword.get(opts, :adapter, []) adapter_options = Pleroma.Config.get([:http, :adapter], []) proxy_url = Pleroma.Config.get([:http, :proxy_url], nil) diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex index c96ee7353..dec24458a 100644 --- a/lib/pleroma/http/http.ex +++ b/lib/pleroma/http/http.ex @@ -65,10 +65,7 @@ defmodule Pleroma.HTTP do end def process_request_options(options) do - case Pleroma.Config.get([:http, :proxy_url]) do - nil -> options - proxy -> options ++ [proxy: proxy] - end + Keyword.merge(Pleroma.HTTP.Connection.hackney_options([]), options) end @doc """ diff --git a/lib/pleroma/reverse_proxy/reverse_proxy.ex b/lib/pleroma/reverse_proxy/reverse_proxy.ex index bf31e9cba..1f98f215c 100644 --- a/lib/pleroma/reverse_proxy/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy/reverse_proxy.ex @@ -61,7 +61,7 @@ defmodule Pleroma.ReverseProxy do * `http`: options for [hackney](https://github.com/benoitc/hackney). """ - @default_hackney_options [] + @default_hackney_options [pool: :media] @inline_content_types [ "image/gif", @@ -94,7 +94,8 @@ defmodule Pleroma.ReverseProxy do def call(conn = %{method: method}, url, opts) when method in @methods do hackney_opts = - @default_hackney_options + Pleroma.HTTP.Connection.hackney_options([]) + |> Keyword.merge(@default_hackney_options) |> Keyword.merge(Keyword.get(opts, :http, [])) |> HTTP.process_request_options() -- cgit v1.2.3 From 369e9bb42fc907f2e3f92e7e44dc52d6940dc046 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 13 Jul 2019 14:49:39 +0300 Subject: [#1041] Rate-limited status actions (per user and per user+status). --- lib/pleroma/plugs/rate_limiter.ex | 67 +++++++++++++++++----- .../web/mastodon_api/mastodon_api_controller.ex | 22 ++++++- 2 files changed, 73 insertions(+), 16 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/plugs/rate_limiter.ex b/lib/pleroma/plugs/rate_limiter.ex index c5e0957e8..31388f574 100644 --- a/lib/pleroma/plugs/rate_limiter.ex +++ b/lib/pleroma/plugs/rate_limiter.ex @@ -31,12 +31,28 @@ defmodule Pleroma.Plugs.RateLimiter do ## Usage + AllowedSyntax: + + plug(Pleroma.Plugs.RateLimiter, :limiter_name) + plug(Pleroma.Plugs.RateLimiter, {:limiter_name, options}) + + Allowed options: + + * `bucket_name` overrides bucket name (e.g. to have a separate limit for a set of actions) + * `params` appends values of specified request params (e.g. ["id"]) to bucket name + Inside a controller: plug(Pleroma.Plugs.RateLimiter, :one when action == :one) plug(Pleroma.Plugs.RateLimiter, :two when action in [:two, :three]) - or inside a router pipiline: + plug( + Pleroma.Plugs.RateLimiter, + {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} + when action in ~w(fav_status unfav_status)a + ) + + or inside a router pipeline: pipeline :api do ... @@ -49,33 +65,56 @@ defmodule Pleroma.Plugs.RateLimiter do alias Pleroma.User - def init(limiter_name) do + def init(limiter_name) when is_atom(limiter_name) do + init({limiter_name, []}) + end + + def init({limiter_name, opts}) do case Pleroma.Config.get([:rate_limit, limiter_name]) do nil -> nil - config -> {limiter_name, config} + config -> {limiter_name, config, opts} end end - # do not limit if there is no limiter configuration + # Do not limit if there is no limiter configuration def call(conn, nil), do: conn - def call(conn, opts) do - case check_rate(conn, opts) do - {:ok, _count} -> conn - {:error, _count} -> render_throttled_error(conn) + def call(conn, settings) do + case check_rate(conn, settings) do + {:ok, _count} -> + conn + + {:error, _count} -> + render_throttled_error(conn) + end + end + + defp bucket_name(conn, limiter_name, opts) do + bucket_name = opts[:bucket_name] || limiter_name + + if params_names = opts[:params] do + params_values = for p <- Enum.sort(params_names), do: conn.params[p] + Enum.join([bucket_name] ++ params_values, ":") + else + bucket_name end end - defp check_rate(%{assigns: %{user: %User{id: user_id}}}, {limiter_name, [_, {scale, limit}]}) do - ExRated.check_rate("#{limiter_name}:#{user_id}", scale, limit) + defp check_rate( + %{assigns: %{user: %User{id: user_id}}} = conn, + {limiter_name, [_, {scale, limit}], opts} + ) do + bucket_name = bucket_name(conn, limiter_name, opts) + ExRated.check_rate("#{bucket_name}:#{user_id}", scale, limit) end - defp check_rate(conn, {limiter_name, [{scale, limit} | _]}) do - ExRated.check_rate("#{limiter_name}:#{ip(conn)}", scale, limit) + defp check_rate(conn, {limiter_name, [{scale, limit} | _], opts}) do + bucket_name = bucket_name(conn, limiter_name, opts) + ExRated.check_rate("#{bucket_name}:#{ip(conn)}", scale, limit) end - defp check_rate(conn, {limiter_name, {scale, limit}}) do - check_rate(conn, {limiter_name, [{scale, limit}]}) + defp check_rate(conn, {limiter_name, {scale, limit}, opts}) do + check_rate(conn, {limiter_name, [{scale, limit}, {scale, limit}], opts}) end def ip(%{remote_ip: remote_ip}) do diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 8c2033c3a..76648b9f7 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Pagination + alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.ScheduledActivity alias Pleroma.Stats @@ -46,8 +47,25 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do require Logger - plug(Pleroma.Plugs.RateLimiter, :app_account_creation when action == :account_register) - plug(Pleroma.Plugs.RateLimiter, :search when action in [:search, :search2, :account_search]) + @rate_limited_status_crud_actions ~w(post_status delete_status)a + @rate_limited_status_reactions ~w(reblog_status unreblog_status fav_status unfav_status)a + @rate_limited_status_actions @rate_limited_status_crud_actions ++ @rate_limited_status_reactions + + plug( + RateLimiter, + {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]} + when action in ~w(reblog_status unreblog_status)a + ) + + plug( + RateLimiter, + {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} + when action in ~w(fav_status unfav_status)a + ) + + plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions) + plug(RateLimiter, :app_account_creation when action == :account_register) + plug(RateLimiter, :search when action in [:search, :search2, :account_search]) @local_mastodon_name "Mastodon-Local" -- cgit v1.2.3 From d72876c57dd8f519f63f7bb14abcfaceedf41410 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 13 Jul 2019 15:21:50 +0300 Subject: [#1041] Minor refactoring. --- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 76648b9f7..8a7b75025 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -47,9 +47,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do require Logger - @rate_limited_status_crud_actions ~w(post_status delete_status)a - @rate_limited_status_reactions ~w(reblog_status unreblog_status fav_status unfav_status)a - @rate_limited_status_actions @rate_limited_status_crud_actions ++ @rate_limited_status_reactions + @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status + post_status delete_status)a plug( RateLimiter, -- cgit v1.2.3 From 80c46d6d8b84d77d86efc32c1d2af225c1eada33 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sat, 13 Jul 2019 18:30:45 +0000 Subject: nodeinfo: implement MRF transparency exclusions --- lib/pleroma/web/nodeinfo/nodeinfo_controller.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index cd9a4f4a8..a1d7fcc7d 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -34,8 +34,11 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do def raw_nodeinfo do stats = Stats.get_stats() + exclusions = Config.get([:instance, :mrf_transparency_exclusions]) + mrf_simple = Config.get(:mrf_simple) + |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end) |> Enum.into(%{}) # This horror is needed to convert regex sigils to strings @@ -86,7 +89,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do mrf_simple: mrf_simple, mrf_keyword: mrf_keyword, mrf_user_allowlist: mrf_user_allowlist, - quarantined_instances: quarantined + quarantined_instances: quarantined, + exclusions: length(exclusions) > 0 } else %{} -- cgit v1.2.3 From f4447d82b814e4710a0d7499bc0707773ac1e440 Mon Sep 17 00:00:00 2001 From: Alex S Date: Thu, 11 Jul 2019 16:04:42 +0300 Subject: parsers configurable --- lib/pleroma/web/rich_media/parser.ex | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 21cd47890..0d2523338 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -3,12 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.RichMedia.Parser do - @parsers [ - Pleroma.Web.RichMedia.Parsers.OGP, - Pleroma.Web.RichMedia.Parsers.TwitterCard, - Pleroma.Web.RichMedia.Parsers.OEmbed - ] - @hackney_options [ pool: :media, recv_timeout: 2_000, @@ -16,6 +10,10 @@ defmodule Pleroma.Web.RichMedia.Parser do with_body: true ] + defp parsers do + Pleroma.Config.get([:rich_media, :parsers]) + end + def parse(nil), do: {:error, "No URL provided"} if Pleroma.Config.get(:env) == :test do @@ -48,7 +46,7 @@ defmodule Pleroma.Web.RichMedia.Parser do end defp maybe_parse(html) do - Enum.reduce_while(@parsers, %{}, fn parser, acc -> + Enum.reduce_while(parsers(), %{}, fn parser, acc -> case parser.parse(html, acc) do {:ok, data} -> {:halt, data} {:error, _msg} -> {:cont, acc} -- cgit v1.2.3 From 40d0a198e20d577b51339f4026d672b1aa968be1 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 14 Jul 2019 12:02:16 +0200 Subject: Object.Fetcher: Handle error on Containment.contain_origin/2 --- lib/pleroma/object/fetcher.ex | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'lib') diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 101c21f96..82250ab8d 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -38,6 +38,7 @@ defmodule Pleroma.Object.Fetcher do "type" => "Create", "to" => data["to"], "cc" => data["cc"], + # TODO: Should we seriously keep this attributedTo thing? "actor" => data["actor"] || data["attributedTo"], "object" => data }, @@ -56,6 +57,9 @@ defmodule Pleroma.Object.Fetcher do object = %Object{} -> {:ok, object} + :error -> + {:error, "Object containment failed."} + _e -> Logger.info("Couldn't get object via AP, trying out OStatus fetching...") -- cgit v1.2.3 From e7c39b7ac8f0462ab563d3cf51f24c76feab0e8d Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sun, 14 Jul 2019 13:29:31 +0000 Subject: Feature/1072 muting notifications --- lib/pleroma/notification.ex | 70 ++++++++++++---------- lib/pleroma/user.ex | 21 +++++-- lib/pleroma/user/info.ex | 28 +++++++++ lib/pleroma/web/mastodon_api/mastodon_api.ex | 5 +- .../web/mastodon_api/mastodon_api_controller.ex | 9 ++- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 +- .../web/twitter_api/twitter_api_controller.ex | 7 +++ 7 files changed, 99 insertions(+), 43 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index a414afbbf..ee7b37aab 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -11,7 +11,6 @@ defmodule Pleroma.Notification do alias Pleroma.Pagination alias Pleroma.Repo alias Pleroma.User - alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.Push alias Pleroma.Web.Streamer @@ -32,31 +31,47 @@ defmodule Pleroma.Notification do |> cast(attrs, [:seen]) end - def for_user_query(user) do - Notification - |> where(user_id: ^user.id) - |> where( - [n, a], - fragment( - "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')", - a.actor - ) - ) - |> join(:inner, [n], activity in assoc(n, :activity)) - |> join(:left, [n, a], object in Object, - on: + def for_user_query(user, opts) do + query = + Notification + |> where(user_id: ^user.id) + |> where( + [n, a], fragment( - "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)", - object.data, - a.data + "? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')", + a.actor ) - ) - |> preload([n, a, o], activity: {a, object: o}) + ) + |> join(:inner, [n], activity in assoc(n, :activity)) + |> join(:left, [n, a], object in Object, + on: + fragment( + "(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)", + object.data, + a.data + ) + ) + |> preload([n, a, o], activity: {a, object: o}) + + if opts[:with_muted] do + query + else + where(query, [n, a], a.actor not in ^user.info.muted_notifications) + |> where([n, a], a.actor not in ^user.info.blocks) + |> where( + [n, a], + fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks + ) + |> join(:left, [n, a], tm in Pleroma.ThreadMute, + on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data) + ) + |> where([n, a, o, tm], is_nil(tm.id)) + end end def for_user(user, opts \\ %{}) do user - |> for_user_query() + |> for_user_query(opts) |> Pagination.fetch_paginated(opts) end @@ -179,11 +194,10 @@ defmodule Pleroma.Notification do def get_notified_from_activity(_, _local_only), do: [] + @spec skip?(Activity.t(), User.t()) :: boolean() def skip?(activity, user) do [ :self, - :blocked, - :muted, :followers, :follows, :non_followers, @@ -193,21 +207,11 @@ defmodule Pleroma.Notification do |> Enum.any?(&skip?(&1, activity, user)) end + @spec skip?(atom(), Activity.t(), User.t()) :: boolean() def skip?(:self, activity, user) do activity.data["actor"] == user.ap_id end - def skip?(:blocked, activity, user) do - actor = activity.data["actor"] - User.blocks?(user, %{ap_id: actor}) - end - - def skip?(:muted, activity, user) do - actor = activity.data["actor"] - - User.mutes?(user, %{ap_id: actor}) or CommonAPI.thread_muted?(user, activity) - end - def skip?( :followers, activity, diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index e5a6c2529..29c87d4a9 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -749,10 +749,13 @@ defmodule Pleroma.User do |> Repo.all() end - def mute(muter, %User{ap_id: ap_id}) do + @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()} + def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do + info = muter.info + info_cng = - muter.info - |> User.Info.add_to_mutes(ap_id) + User.Info.add_to_mutes(info, ap_id) + |> User.Info.add_to_muted_notifications(info, ap_id, notifications?) cng = change(muter) @@ -762,9 +765,11 @@ defmodule Pleroma.User do end def unmute(muter, %{ap_id: ap_id}) do + info = muter.info + info_cng = - muter.info - |> User.Info.remove_from_mutes(ap_id) + User.Info.remove_from_mutes(info, ap_id) + |> User.Info.remove_from_muted_notifications(info, ap_id) cng = change(muter) @@ -860,6 +865,12 @@ defmodule Pleroma.User do def mutes?(nil, _), do: false def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id) + @spec muted_notifications?(User.t() | nil, User.t() | map()) :: boolean() + def muted_notifications?(nil, _), do: false + + def muted_notifications?(user, %{ap_id: ap_id}), + do: Enum.member?(user.info.muted_notifications, ap_id) + def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do blocks = info.blocks domain_blocks = info.domain_blocks diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 08e43ff0f..9beb3ddbd 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -24,6 +24,7 @@ defmodule Pleroma.User.Info do field(:domain_blocks, {:array, :string}, default: []) field(:mutes, {:array, :string}, default: []) field(:muted_reblogs, {:array, :string}, default: []) + field(:muted_notifications, {:array, :string}, default: []) field(:subscribers, {:array, :string}, default: []) field(:deactivated, :boolean, default: false) field(:no_rich_text, :boolean, default: false) @@ -120,6 +121,16 @@ defmodule Pleroma.User.Info do |> validate_required([:mutes]) end + @spec set_notification_mutes(Changeset.t(), [String.t()], boolean()) :: Changeset.t() + def set_notification_mutes(changeset, muted_notifications, notifications?) do + if notifications? do + put_change(changeset, :muted_notifications, muted_notifications) + |> validate_required([:muted_notifications]) + else + changeset + end + end + def set_blocks(info, blocks) do params = %{blocks: blocks} @@ -136,14 +147,31 @@ defmodule Pleroma.User.Info do |> validate_required([:subscribers]) end + @spec add_to_mutes(Info.t(), String.t()) :: Changeset.t() def add_to_mutes(info, muted) do set_mutes(info, Enum.uniq([muted | info.mutes])) end + @spec add_to_muted_notifications(Changeset.t(), Info.t(), String.t(), boolean()) :: + Changeset.t() + def add_to_muted_notifications(changeset, info, muted, notifications?) do + set_notification_mutes( + changeset, + Enum.uniq([muted | info.muted_notifications]), + notifications? + ) + end + + @spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t() def remove_from_mutes(info, muted) do set_mutes(info, List.delete(info.mutes, muted)) end + @spec remove_from_muted_notifications(Changeset.t(), Info.t(), String.t()) :: Changeset.t() + def remove_from_muted_notifications(changeset, info, muted) do + set_notification_mutes(changeset, List.delete(info.muted_notifications, muted), true) + end + def add_to_block(info, blocked) do set_blocks(info, Enum.uniq([blocked | info.blocks])) end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index c82b20123..46944dcbc 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -53,7 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do options = cast_params(params) user - |> Notification.for_user_query() + |> Notification.for_user_query(options) |> restrict(:exclude_types, options) |> Pagination.fetch_paginated(params) end @@ -67,7 +67,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do defp cast_params(params) do param_types = %{ exclude_types: {:array, :string}, - reblogs: :boolean + reblogs: :boolean, + with_muted: :boolean } changeset = cast({%{}, param_types}, params, Map.keys(param_types)) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 8a7b75025..b3513b5bf 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1068,9 +1068,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do + def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do + notifications = + if Map.has_key?(params, "notifications"), + do: params["notifications"] in [true, "True", "true", "1"], + else: true + with %User{} = muted <- User.get_cached_by_id(id), - {:ok, muter} <- User.mute(muter, muted) do + {:ok, muter} <- User.mute(muter, muted, notifications) do conn |> put_view(AccountView) |> render("relationship.json", %{user: muter, target: muted}) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 62c516f8e..65bab4062 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -52,7 +52,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do followed_by: User.following?(target, user), blocking: User.blocks?(user, target), muting: User.mutes?(user, target), - muting_notifications: false, + muting_notifications: User.muted_notifications?(user, target), subscribing: User.subscribed_to?(user, target), requested: requested, domain_blocking: false, diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 45ef7be3d..0313560a8 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -192,6 +192,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end def notifications(%{assigns: %{user: user}} = conn, params) do + params = + if Map.has_key?(params, "with_muted") do + Map.put(params, :with_muted, params["with_muted"] in [true, "True", "true", "1"]) + else + params + end + notifications = Notification.for_user(user, params) conn -- cgit v1.2.3 From e1c08a67d6f8981417fe4d5592a60a3882f454f9 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 14 Jul 2019 12:13:11 +0200 Subject: Object.Fetcher: Fallback to OStatus only if AP actually fails --- lib/pleroma/object/fetcher.ex | 60 +++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 25 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 82250ab8d..14454ce9d 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -31,42 +31,52 @@ defmodule Pleroma.Object.Fetcher do {:ok, object} else Logger.info("Fetching #{id} via AP") + {status, data} = fetch_and_contain_remote_object_from_id(id) + object = Object.normalize(data, false) - with {:ok, data} <- fetch_and_contain_remote_object_from_id(id), - nil <- Object.normalize(data, false), - params <- %{ - "type" => "Create", - "to" => data["to"], - "cc" => data["cc"], - # TODO: Should we seriously keep this attributedTo thing? - "actor" => data["actor"] || data["attributedTo"], - "object" => data - }, - :ok <- Containment.contain_origin(id, params), - {:ok, activity} <- Transmogrifier.handle_incoming(params, options), - {:object, _data, %Object{} = object} <- - {:object, data, Object.normalize(activity, false)} do - {:ok, object} - else - {:error, {:reject, nil}} -> - {:reject, nil} + if status == :ok and object == nil do + with params <- %{ + "type" => "Create", + "to" => data["to"], + "cc" => data["cc"], + # Should we seriously keep this attributedTo thing? + "actor" => data["actor"] || data["attributedTo"], + "object" => data + }, + :ok <- Containment.contain_origin(id, params), + {:ok, activity} <- Transmogrifier.handle_incoming(params, options), + {:object, _data, %Object{} = object} <- + {:object, data, Object.normalize(activity, false)} do + {:ok, object} + else + {:error, {:reject, nil}} -> + {:reject, nil} - {:object, data, nil} -> - reinject_object(data) + {:object, data, nil} -> + reinject_object(data) - object = %Object{} -> - {:ok, object} + object = %Object{} -> + {:ok, object} - :error -> - {:error, "Object containment failed."} + :error -> + {:error, "Object containment failed."} - _e -> + e -> + e + end + else + if status == :ok and object != nil do + {:ok, object} + else + # Only fallback when receiving a fetch/normalization error with ActivityPub Logger.info("Couldn't get object via AP, trying out OStatus fetching...") + # FIXME: OStatus Object Containment? case OStatus.fetch_activity_from_url(id) do {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)} e -> e end + end end end end -- cgit v1.2.3 From 2592934480dd704033de013491373c9dc1d173a2 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sun, 14 Jul 2019 17:28:25 +0200 Subject: Object.Fetcher: Keep the with-do block as per kaniini proposition --- lib/pleroma/object/fetcher.ex | 58 +++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 33 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 14454ce9d..96b34ae9f 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -31,43 +31,36 @@ defmodule Pleroma.Object.Fetcher do {:ok, object} else Logger.info("Fetching #{id} via AP") - {status, data} = fetch_and_contain_remote_object_from_id(id) - object = Object.normalize(data, false) - if status == :ok and object == nil do - with params <- %{ - "type" => "Create", - "to" => data["to"], - "cc" => data["cc"], - # Should we seriously keep this attributedTo thing? - "actor" => data["actor"] || data["attributedTo"], - "object" => data - }, - :ok <- Containment.contain_origin(id, params), - {:ok, activity} <- Transmogrifier.handle_incoming(params, options), - {:object, _data, %Object{} = object} <- - {:object, data, Object.normalize(activity, false)} do - {:ok, object} - else - {:error, {:reject, nil}} -> - {:reject, nil} - - {:object, data, nil} -> - reinject_object(data) + with {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)}, + {:normalize, nil} <- {:normalize, Object.normalize(data, false)}, + params <- %{ + "type" => "Create", + "to" => data["to"], + "cc" => data["cc"], + # Should we seriously keep this attributedTo thing? + "actor" => data["actor"] || data["attributedTo"], + "object" => data + }, + {:containment, :ok} <- {:containment, Containment.contain_origin(id, params)}, + {:ok, activity} <- Transmogrifier.handle_incoming(params, options), + {:object, _data, %Object{} = object} <- + {:object, data, Object.normalize(activity, false)} do + {:ok, object} + else + {:containment, _} -> + {:error, "Object containment failed."} - object = %Object{} -> - {:ok, object} + {:error, {:reject, nil}} -> + {:reject, nil} - :error -> - {:error, "Object containment failed."} + {:object, data, nil} -> + reinject_object(data) - e -> - e - end - else - if status == :ok and object != nil do + {:normalize, object = %Object{}} -> {:ok, object} - else + + _e -> # Only fallback when receiving a fetch/normalization error with ActivityPub Logger.info("Couldn't get object via AP, trying out OStatus fetching...") @@ -76,7 +69,6 @@ defmodule Pleroma.Object.Fetcher do {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)} e -> e end - end end end end -- cgit v1.2.3 From 739bbe0d3bbe06ca9d634498ea5909f35fc5ad84 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 14 Jul 2019 17:47:08 +0000 Subject: security: detect object containment violations at the IR level It is more efficient to check for object containment violations at the IR level instead of in the protocol handlers. OStatus containment is especially a tricky situation, as the containment rules don't match those of IR and ActivityPub. Accordingly, we just always do a final containment check at the IR level before the object is added to the IR object graph. --- lib/pleroma/object/containment.ex | 8 ++++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 2 ++ 2 files changed, 10 insertions(+) (limited to 'lib') diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index ada9da0bb..f077a9f32 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -48,6 +48,9 @@ defmodule Pleroma.Object.Containment do end end + def contain_origin(id, %{"attributedTo" => actor} = params), + do: contain_origin(id, Map.put(params, "actor", actor)) + def contain_origin_from_id(_id, %{"id" => nil}), do: :error def contain_origin_from_id(id, %{"id" => other_id} = _params) do @@ -60,4 +63,9 @@ defmodule Pleroma.Object.Containment do :error end end + + def contain_child(%{"object" => %{"id" => id, "attributedTo" => _} = object}), + do: contain_origin(id, object) + + def contain_child(_), do: :ok end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index a3174a787..87963b691 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Conversation alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Object.Containment alias Pleroma.Object.Fetcher alias Pleroma.Pagination alias Pleroma.Repo @@ -126,6 +127,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do {:ok, map} <- MRF.filter(map), {recipients, _, _} = get_recipients(map), {:fake, false, map, recipients} <- {:fake, fake, map, recipients}, + :ok <- Containment.contain_child(map), {:ok, map, object} <- insert_full_object(map) do {:ok, activity} = Repo.insert(%Activity{ -- cgit v1.2.3 From dce8ebc9eabac1a597491a0edc5c145285c55671 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Sun, 14 Jul 2019 19:25:03 +0000 Subject: Unfollow should also unsubscribe --- lib/pleroma/web/common_api/common_api.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index f1450b113..949baa3b0 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -31,7 +31,8 @@ defmodule Pleroma.Web.CommonAPI do def unfollow(follower, unfollowed) do with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), - {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do + {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed), + {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do {:ok, follower} end end -- cgit v1.2.3 From fa17879c204980c6fb0025b2e51a978669c441da Mon Sep 17 00:00:00 2001 From: Maksim Date: Sun, 14 Jul 2019 21:01:32 +0000 Subject: added tests for Web.MediaProxy --- lib/pleroma/web/media_proxy/controller.ex | 39 -------------------- lib/pleroma/web/media_proxy/media_proxy.ex | 39 ++++++++++---------- .../web/media_proxy/media_proxy_controller.ex | 41 ++++++++++++++++++++++ 3 files changed, 62 insertions(+), 57 deletions(-) delete mode 100644 lib/pleroma/web/media_proxy/controller.ex create mode 100644 lib/pleroma/web/media_proxy/media_proxy_controller.ex (limited to 'lib') diff --git a/lib/pleroma/web/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/controller.ex deleted file mode 100644 index ea33d7685..000000000 --- a/lib/pleroma/web/media_proxy/controller.ex +++ /dev/null @@ -1,39 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2019 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.MediaProxy.MediaProxyController do - use Pleroma.Web, :controller - alias Pleroma.ReverseProxy - alias Pleroma.Web.MediaProxy - - @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]] - - def remote(conn, %{"sig" => sig64, "url" => url64} = params) do - with config <- Pleroma.Config.get([:media_proxy], []), - true <- Keyword.get(config, :enabled, false), - {:ok, url} <- MediaProxy.decode_url(sig64, url64), - :ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do - ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts)) - else - false -> - send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) - - {:error, :invalid_signature} -> - send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403)) - - {:wrong_filename, filename} -> - redirect(conn, external: MediaProxy.build_url(sig64, url64, filename)) - end - end - - def filename_matches(has_filename, path, url) do - filename = url |> MediaProxy.filename() - - if has_filename && filename && Path.basename(path) != filename do - {:wrong_filename, filename} - else - :ok - end - end -end diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index dd8888a02..a661e9bb7 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -3,68 +3,71 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MediaProxy do - @base64_opts [padding: false] - - def url(nil), do: nil + alias Pleroma.Config + alias Pleroma.Web - def url(""), do: nil + @base64_opts [padding: false] + def url(url) when is_nil(url) or url == "", do: nil def url("/" <> _ = url), do: url def url(url) do - if !enabled?() or local?(url) or whitelisted?(url) do + if disabled?() or local?(url) or whitelisted?(url) do url else encode_url(url) end end - defp enabled?, do: Pleroma.Config.get([:media_proxy, :enabled], false) + defp disabled?, do: !Config.get([:media_proxy, :enabled], false) defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) defp whitelisted?(url) do %{host: domain} = URI.parse(url) - Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern -> + Enum.any?(Config.get([:media_proxy, :whitelist]), fn pattern -> String.equivalent?(domain, pattern) end) end def encode_url(url) do - secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) base64 = Base.url_encode64(url, @base64_opts) - sig = :crypto.hmac(:sha, secret, base64) - sig64 = sig |> Base.url_encode64(@base64_opts) + + sig64 = + base64 + |> signed_url + |> Base.url_encode64(@base64_opts) build_url(sig64, base64, filename(url)) end def decode_url(sig, url) do - secret = Pleroma.Config.get([Pleroma.Web.Endpoint, :secret_key_base]) - sig = Base.url_decode64!(sig, @base64_opts) - local_sig = :crypto.hmac(:sha, secret, url) - - if local_sig == sig do + with {:ok, sig} <- Base.url_decode64(sig, @base64_opts), + signature when signature == sig <- signed_url(url) do {:ok, Base.url_decode64!(url, @base64_opts)} else - {:error, :invalid_signature} + _ -> {:error, :invalid_signature} end end + defp signed_url(url) do + :crypto.hmac(:sha, Config.get([Web.Endpoint, :secret_key_base]), url) + 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()), + Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()), "proxy", sig_base64, url_base64, filename ] - |> Enum.filter(fn value -> value end) + |> Enum.filter(& &1) |> Path.join() end end diff --git a/lib/pleroma/web/media_proxy/media_proxy_controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex new file mode 100644 index 000000000..1e9520d46 --- /dev/null +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MediaProxy.MediaProxyController do + use Pleroma.Web, :controller + alias Pleroma.ReverseProxy + alias Pleroma.Web.MediaProxy + + @default_proxy_opts [max_body_length: 25 * 1_048_576, http: [follow_redirect: true]] + + def remote(conn, %{"sig" => sig64, "url" => url64} = params) do + with config <- Pleroma.Config.get([:media_proxy], []), + true <- Keyword.get(config, :enabled, false), + {:ok, url} <- MediaProxy.decode_url(sig64, url64), + :ok <- filename_matches(params, conn.request_path, url) do + ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts)) + else + false -> + send_resp(conn, 404, Plug.Conn.Status.reason_phrase(404)) + + {:error, :invalid_signature} -> + send_resp(conn, 403, Plug.Conn.Status.reason_phrase(403)) + + {:wrong_filename, filename} -> + redirect(conn, external: MediaProxy.build_url(sig64, url64, filename)) + end + end + + def filename_matches(%{"filename" => _} = _, path, url) do + filename = MediaProxy.filename(url) + + if filename && Path.basename(path) != filename do + {:wrong_filename, filename} + else + :ok + end + end + + def filename_matches(_, _, _), do: :ok +end -- cgit v1.2.3