diff options
Diffstat (limited to 'lib')
43 files changed, 636 insertions, 248 deletions
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index a71bcd447..a7d0fac5d 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -28,6 +28,14 @@ defmodule Mix.Tasks.Pleroma.Config do |> Enum.reject(fn {k, _v} -> k in [Pleroma.Repo, :env] end) |> Enum.each(fn {k, v} -> key = to_string(k) |> String.replace("Elixir.", "") + + key = + if String.starts_with?(key, "Pleroma.") do + key + else + ":" <> key + end + {:ok, _} = Config.update_or_create(%{group: "pleroma", key: key, value: v}) Mix.shell().info("#{key} is migrated.") end) @@ -53,17 +61,9 @@ defmodule Mix.Tasks.Pleroma.Config do Repo.all(Config) |> Enum.each(fn config -> - mark = - if String.starts_with?(config.key, "Pleroma.") or - String.starts_with?(config.key, "Ueberauth"), - do: ",", - else: ":" - IO.write( file, - "config :#{config.group}, #{config.key}#{mark} #{ - inspect(Config.from_binary(config.value)) - }\r\n" + "config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n" ) if delete? do diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index ba4cf8486..035331491 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -140,6 +140,11 @@ defmodule Pleroma.Application do id: :federator_init, start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]}, restart: :temporary + }, + %{ + id: :internal_fetch_init, + start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]}, + restart: :temporary } ] ++ streamer_child() ++ diff --git a/lib/pleroma/config/transfer_task.ex b/lib/pleroma/config/transfer_task.ex index 3c13a0558..7799b2a78 100644 --- a/lib/pleroma/config/transfer_task.ex +++ b/lib/pleroma/config/transfer_task.ex @@ -35,7 +35,7 @@ defmodule Pleroma.Config.TransferTask do if String.starts_with?(setting.key, "Pleroma.") do "Elixir." <> setting.key else - setting.key + String.trim_leading(setting.key, ":") end group = String.to_existing_atom(setting.group) diff --git a/lib/pleroma/keys.ex b/lib/pleroma/keys.ex index b7bc7a4da..6dd31d3bd 100644 --- a/lib/pleroma/keys.ex +++ b/lib/pleroma/keys.ex @@ -35,10 +35,12 @@ defmodule Pleroma.Keys do end def keys_from_pem(pem) do - [private_key_code] = :public_key.pem_decode(pem) - private_key = :public_key.pem_entry_decode(private_key_code) - {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key - public_key = {:RSAPublicKey, modulus, exponent} - {:ok, private_key, public_key} + with [private_key_code] <- :public_key.pem_decode(pem), + private_key <- :public_key.pem_entry_decode(private_key_code), + {:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} <- private_key do + {:ok, private_key, {:RSAPublicKey, modulus, exponent}} + else + error -> {:error, error} + end end end diff --git a/lib/pleroma/list.ex b/lib/pleroma/list.ex index a5b1cad68..1d320206e 100644 --- a/lib/pleroma/list.ex +++ b/lib/pleroma/list.ex @@ -16,6 +16,7 @@ defmodule Pleroma.List do belongs_to(:user, User, type: Pleroma.FlakeId) field(:title, :string) field(:following, {:array, :string}, default: []) + field(:ap_id, :string) timestamps() end @@ -55,6 +56,10 @@ defmodule Pleroma.List do Repo.one(query) end + def get_by_ap_id(ap_id) do + Repo.get_by(__MODULE__, ap_id: ap_id) + end + def get_following(%Pleroma.List{following: following} = _list) do q = from( @@ -105,7 +110,14 @@ defmodule Pleroma.List do def create(title, %User{} = creator) do list = %Pleroma.List{user_id: creator.id, title: title} - Repo.insert(list) + + Repo.transaction(fn -> + list = Repo.insert!(list) + + list + |> change(ap_id: "#{creator.ap_id}/lists/#{list.id}") + |> Repo.update!() + end) end def follow(%Pleroma.List{following: following} = list, %User{} = followed) do @@ -125,4 +137,19 @@ defmodule Pleroma.List do |> follow_changeset(attrs) |> Repo.update() end + + def memberships(%User{follower_address: follower_address}) do + Pleroma.List + |> where([l], ^follower_address in l.following) + |> select([l], l.ap_id) + |> Repo.all() + end + + def memberships(_), do: [] + + def member?(%Pleroma.List{following: following}, %User{follower_address: follower_address}) do + Enum.member?(following, follower_address) + end + + def member?(_, _), do: false end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index a414afbbf..d47229258 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.user_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/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/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 101c21f96..305ce8357 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Object.Fetcher do alias Pleroma.HTTP alias Pleroma.Object alias Pleroma.Object.Containment + alias Pleroma.Signature + alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.OStatus @@ -32,33 +34,39 @@ defmodule Pleroma.Object.Fetcher do else Logger.info("Fetching #{id} via AP") - with {:ok, data} <- fetch_and_contain_remote_object_from_id(id), - nil <- Object.normalize(data, false), + 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 }, - :ok <- Containment.contain_origin(id, params), + {: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."} + {:error, {:reject, nil}} -> {:reject, nil} {:object, data, nil} -> reinject_object(data) - object = %Object{} -> + {:normalize, object = %Object{}} -> {:ok, object} _e -> + # 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 @@ -76,15 +84,52 @@ defmodule Pleroma.Object.Fetcher do end end + defp make_signature(id, date) do + uri = URI.parse(id) + + signature = + InternalFetchActor.get_actor() + |> Signature.sign(%{ + "(request-target)": "get #{uri.path}", + host: uri.host, + date: date + }) + + [{:Signature, signature}] + end + + defp sign_fetch(headers, id, date) do + if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do + headers ++ make_signature(id, date) + else + headers + end + end + + defp maybe_date_fetch(headers, date) do + if Pleroma.Config.get([:activitypub, :sign_object_fetches]) do + headers ++ [{:Date, date}] + else + headers + end + end + def fetch_and_contain_remote_object_from_id(id) do Logger.info("Fetching object #{id} via AP") + date = + NaiveDateTime.utc_now() + |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") + + headers = + [{:Accept, "application/activity+json"}] + |> maybe_date_fetch(date) + |> sign_fetch(id, date) + + Logger.debug("Fetch headers: #{inspect(headers)}") + with true <- String.starts_with?(id, "http"), - {:ok, %{body: body, status: code}} when code in 200..299 <- - HTTP.get( - id, - [{:Accept, "application/activity+json"}] - ), + {:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get(id, headers), {:ok, data} <- Jason.decode(body), :ok <- Containment.contain_origin_from_id(id, data) do {:ok, data} diff --git a/lib/pleroma/plugs/authentication_plug.ex b/lib/pleroma/plugs/authentication_plug.ex index da4ed4226..567674a0b 100644 --- a/lib/pleroma/plugs/authentication_plug.ex +++ b/lib/pleroma/plugs/authentication_plug.ex @@ -6,9 +6,21 @@ defmodule Pleroma.Plugs.AuthenticationPlug do alias Comeonin.Pbkdf2 import Plug.Conn alias Pleroma.User + require Logger - def init(options) do - options + def init(options), do: options + + def checkpw(password, "$6" <> _ = password_hash) do + :crypt.crypt(password, password_hash) == password_hash + end + + def checkpw(password, "$pbkdf2" <> _ = password_hash) do + Pbkdf2.checkpw(password, password_hash) + end + + def checkpw(_password, _password_hash) do + Logger.error("Password hash not recognized") + false end def call(%{assigns: %{user: %User{}}} = conn, _), do: conn diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index 1a4d54c62..a45c70a9d 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -8,10 +8,16 @@ defmodule Pleroma.Signature do alias Pleroma.Keys alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Utils + + defp key_id_to_actor_id(key_id) do + URI.parse(key_id) + |> Map.put(:fragment, nil) + |> URI.to_string() + end def fetch_public_key(conn) do - with actor_id <- Utils.get_ap_id(conn.params["actor"]), + with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), + actor_id <- key_id_to_actor_id(kid), {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} else @@ -21,7 +27,8 @@ defmodule Pleroma.Signature do end def refetch_public_key(conn) do - with actor_id <- Utils.get_ap_id(conn.params["actor"]), + with %{"keyId" => kid} <- HTTPSignatures.signature_for_conn(conn), + actor_id <- key_id_to_actor_id(kid), {:ok, _user} <- ActivityPub.make_user_from_ap_id(actor_id), {:ok, public_key} <- User.get_public_key_for_ap_id(actor_id) do {:ok, public_key} diff --git a/lib/pleroma/upload/filter/dedupe.ex b/lib/pleroma/upload/filter/dedupe.ex index e4c225833..14928c355 100644 --- a/lib/pleroma/upload/filter/dedupe.ex +++ b/lib/pleroma/upload/filter/dedupe.ex @@ -6,10 +6,19 @@ defmodule Pleroma.Upload.Filter.Dedupe do @behaviour Pleroma.Upload.Filter alias Pleroma.Upload - def filter(%Upload{name: name} = upload) do - extension = String.split(name, ".") |> List.last() - shasum = :crypto.hash(:sha256, File.read!(upload.tempfile)) |> Base.encode16(case: :lower) + def filter(%Upload{name: name, tempfile: tempfile} = upload) do + extension = + name + |> String.split(".") + |> List.last() + + shasum = + :crypto.hash(:sha256, File.read!(tempfile)) + |> Base.encode16(case: :lower) + filename = shasum <> "." <> extension {:ok, %Upload{upload | id: shasum, path: filename}} end + + def filter(_), do: :ok end diff --git a/lib/pleroma/upload/filter/mogrifun.ex b/lib/pleroma/upload/filter/mogrifun.ex index 35a5a1381..fee49fb51 100644 --- a/lib/pleroma/upload/filter/mogrifun.ex +++ b/lib/pleroma/upload/filter/mogrifun.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Upload.Filter.Mogrifun do @behaviour Pleroma.Upload.Filter + alias Pleroma.Upload.Filter @filters [ {"implode", "1"}, @@ -34,31 +35,10 @@ defmodule Pleroma.Upload.Filter.Mogrifun do ] 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) + Filter.Mogrify.do_filter(file, [Enum.random(@filters)]) :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 index f459eeecb..91bfdd4f5 100644 --- a/lib/pleroma/upload/filter/mogrify.ex +++ b/lib/pleroma/upload/filter/mogrify.ex @@ -11,16 +11,19 @@ defmodule Pleroma.Upload.Filter.Mogrify do def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do filters = Pleroma.Config.get!([__MODULE__, :args]) + do_filter(file, filters) + :ok + end + + def filter(_), do: :ok + + def do_filter(file, filters) do 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 diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex index 0af76bc59..c0b22c28a 100644 --- a/lib/pleroma/uploaders/uploader.ex +++ b/lib/pleroma/uploaders/uploader.ex @@ -68,7 +68,14 @@ defmodule Pleroma.Uploaders.Uploader do {:error, error} end after - 30_000 -> {:error, dgettext("errors", "Uploader callback timeout")} + callback_timeout() -> {:error, dgettext("errors", "Uploader callback timeout")} + end + end + + defp callback_timeout do + case Mix.env() do + :test -> 1_000 + _ -> 30_000 end end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index e5a6c2529..c91fbb68a 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 @@ -1146,19 +1157,18 @@ defmodule Pleroma.User do end end - def get_or_create_instance_user do - relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay" - - if user = get_cached_by_ap_id(relay_uri) do + @doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing." + def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do + if user = get_cached_by_ap_id(uri) do user else changes = %User{info: %User.Info{}} |> cast(%{}, [:ap_id, :nickname, :local]) - |> put_change(:ap_id, relay_uri) - |> put_change(:nickname, nil) + |> put_change(:ap_id, uri) + |> put_change(:nickname, nickname) |> put_change(:local, true) - |> put_change(:follower_address, relay_uri <> "/followers") + |> put_change(:follower_address, uri <> "/followers") {:ok, user} = Repo.insert(changes) user @@ -1179,10 +1189,12 @@ 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}) when not is_nil(magic_key) do {:ok, Pleroma.Web.Salmon.decode_key(magic_key)} end + def public_key_from_info(_), do: {:error, "not found key"} + def get_public_key_for_ap_id(ap_id) do with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), {:ok, public_key} <- public_key_from_info(user.info) do @@ -1368,23 +1380,16 @@ defmodule Pleroma.User do } end - def ensure_keys_present(user) do - info = user.info - + def ensure_keys_present(%User{info: info} = user) do if info.keys do {:ok, user} else {:ok, pem} = Keys.generate_rsa_pem() - info_cng = - info - |> User.Info.set_keys(pem) - - cng = - Ecto.Changeset.change(user) - |> Ecto.Changeset.put_embed(:info, info_cng) - - update_and_set_cache(cng) + user + |> Ecto.Changeset.change() + |> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem)) + |> update_and_set_cache() end end @@ -1405,4 +1410,8 @@ defmodule Pleroma.User do end defp put_password_hash(changeset), do: changeset + + def is_internal_user?(%User{nickname: nil}), do: true + def is_internal_user?(%User{local: true, nickname: "internal." <> _}), do: true + def is_internal_user?(_), do: false end 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/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index a3174a787..31397b09f 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 @@ -26,19 +27,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do # For Announce activities, we filter the recipients based on following status for any actors # that match actual users. See issue #164 for more information about why this is necessary. defp get_recipients(%{"type" => "Announce"} = data) do - to = data["to"] || [] - cc = data["cc"] || [] + to = Map.get(data, "to", []) + cc = Map.get(data, "cc", []) + bcc = Map.get(data, "bcc", []) actor = User.get_cached_by_ap_id(data["actor"]) recipients = - (to ++ cc) - |> Enum.filter(fn recipient -> + Enum.filter(Enum.concat([to, cc, bcc]), fn recipient -> case User.get_cached_by_ap_id(recipient) do - nil -> - true - - user -> - User.following?(user, actor) + nil -> true + user -> User.following?(user, actor) end end) @@ -46,17 +44,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end defp get_recipients(%{"type" => "Create"} = data) do - to = data["to"] || [] - cc = data["cc"] || [] - actor = data["actor"] || [] - recipients = (to ++ cc ++ [actor]) |> Enum.uniq() + to = Map.get(data, "to", []) + cc = Map.get(data, "cc", []) + bcc = Map.get(data, "bcc", []) + actor = Map.get(data, "actor", []) + recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq() {recipients, to, cc} end defp get_recipients(data) do - to = data["to"] || [] - cc = data["cc"] || [] - recipients = to ++ cc + to = Map.get(data, "to", []) + cc = Map.get(data, "cc", []) + bcc = Map.get(data, "bcc", []) + recipients = Enum.concat([to, cc, bcc]) {recipients, to, cc} end @@ -126,6 +126,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{ @@ -896,13 +897,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp maybe_order(query, _), do: query def fetch_activities_query(recipients, opts \\ %{}) do - base_query = from(activity in Activity) - config = %{ skip_thread_containment: Config.get([:instance, :skip_thread_containment]) } - base_query + Activity |> maybe_preload_objects(opts) |> maybe_preload_bookmarks(opts) |> maybe_set_thread_muted_field(opts) @@ -931,11 +930,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def fetch_activities(recipients, opts \\ %{}) do - fetch_activities_query(recipients, opts) + list_memberships = Pleroma.List.memberships(opts["user"]) + + fetch_activities_query(recipients ++ list_memberships, opts) |> Pagination.fetch_paginated(opts) |> Enum.reverse() + |> maybe_update_cc(list_memberships, opts["user"]) end + defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) + when is_list(list_memberships) and length(list_memberships) > 0 do + Enum.map(activities, fn + %{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 -> + if Enum.any?(bcc, &(&1 in list_memberships)) do + update_in(activity.data["cc"], &[user_ap_id | &1]) + else + activity + end + + activity -> + activity + end) + end + + defp maybe_update_cc(activities, _, _), do: activities + def fetch_activities_bounded_query(query, recipients, recipients_with_public) do from(activity in query, where: diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index e2af4ad1a..133a726c5 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do alias Pleroma.Object.Fetcher alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Transmogrifier @@ -206,9 +207,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do json(conn, dgettext("errors", "error")) end - def relay(conn, _params) do - with %User{} = user <- Relay.get_actor(), - {:ok, user} <- User.ensure_keys_present(user) do + defp represent_service_actor(%User{} = user, conn) do + with {:ok, user} <- User.ensure_keys_present(user) do conn |> put_resp_header("content-type", "application/activity+json") |> json(UserView.render("user.json", %{user: user})) @@ -217,6 +217,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end + defp represent_service_actor(nil, _), do: {:error, :not_found} + + def relay(conn, _params) do + Relay.get_actor() + |> represent_service_actor(conn) + end + + def internal_fetch(conn, _params) do + InternalFetchActor.get_actor() + |> represent_service_actor(conn) + end + def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do conn |> put_resp_header("content-type", "application/activity+json") diff --git a/lib/pleroma/web/activity_pub/internal_fetch_actor.ex b/lib/pleroma/web/activity_pub/internal_fetch_actor.ex new file mode 100644 index 000000000..9213ddde7 --- /dev/null +++ b/lib/pleroma/web/activity_pub/internal_fetch_actor.ex @@ -0,0 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.InternalFetchActor do + alias Pleroma.User + + require Logger + + def init do + # Wait for everything to settle. + Process.sleep(1000 * 5) + get_actor() + end + + def get_actor do + "#{Pleroma.Web.Endpoint.url()}/internal/fetch" + |> User.get_or_create_service_actor_by_ap_id("internal.fetch") + end +end diff --git a/lib/pleroma/web/activity_pub/mrf/mention_policy.ex b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex new file mode 100644 index 000000000..1842e1aeb --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/mention_policy.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.MentionPolicy do + @moduledoc "Block messages which mention a user" + + @behaviour Pleroma.Web.ActivityPub.MRF + + @impl true + def filter(%{"type" => "Create"} = message) do + reject_actors = Pleroma.Config.get([:mrf_mention, :actors], []) + recipients = (message["to"] || []) ++ (message["cc"] || []) + + if Enum.any?(recipients, fn recipient -> Enum.member?(reject_actors, recipient) end) do + {:reject, nil} + else + {:ok, message} + end + end + + @impl true + def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index a05e03263..c505223f7 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -92,18 +92,68 @@ defmodule Pleroma.Web.ActivityPub.Publisher do end end - @doc """ - Publishes an activity to all relevant peers. - """ - def publish(%User{} = actor, %Activity{} = activity) do - remote_followers = + defp recipients(actor, activity) do + followers = if actor.follower_address in activity.recipients do {:ok, followers} = User.get_followers(actor) - followers |> Enum.filter(&(!&1.local)) + Enum.filter(followers, &(!&1.local)) else [] end + Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers + end + + defp get_cc_ap_ids(ap_id, recipients) do + host = Map.get(URI.parse(ap_id), :host) + + recipients + |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end) + |> Enum.map(& &1.ap_id) + end + + @doc """ + Publishes an activity with BCC to all relevant peers. + """ + + def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do + public = is_public?(activity) + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + + recipients = recipients(actor, activity) + + recipients + |> Enum.filter(&User.ap_enabled?/1) + |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end) + |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) + |> Instances.filter_reachable() + |> Enum.each(fn {inbox, unreachable_since} -> + %User{ap_id: ap_id} = + Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end) + + # Get all the recipients on the same host and add them to cc. Otherwise, a remote + # instance would only accept a first message for the first recipient and ignore the rest. + cc = get_cc_ap_ids(ap_id, recipients) + + json = + data + |> Map.put("cc", cc) + |> Jason.encode!() + + Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{ + inbox: inbox, + json: json, + actor: actor, + id: activity.data["id"], + unreachable_since: unreachable_since + }) + end) + end + + @doc """ + Publishes an activity to all relevant peers. + """ + def publish(%User{} = actor, %Activity{} = activity) do public = is_public?(activity) if public && Config.get([:instance, :allow_relay]) do @@ -114,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) json = Jason.encode!(data) - (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers) + recipients(actor, activity) |> Enum.filter(fn user -> User.ap_enabled?(user) end) |> Enum.map(fn %{info: %{source_data: data}} -> (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 93808517b..1ebfcdd86 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -10,7 +10,8 @@ defmodule Pleroma.Web.ActivityPub.Relay do require Logger def get_actor do - User.get_or_create_instance_user() + "#{Pleroma.Web.Endpoint.url()}/relay" + |> User.get_or_create_service_actor_by_ap_id() end def follow(target_instance) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d14490bb5..602ae48e1 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -814,13 +814,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do object = - Object.normalize(object_id).data + object_id + |> Object.normalize() + |> Map.get(:data) |> prepare_object data = data |> Map.put("object", object) |> Map.merge(Utils.make_json_ld_header()) + |> Map.delete("bcc") {:ok, data} end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 4288ea4c8..c146f59d4 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -25,12 +25,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do # Some implementations send the actor URI as the actor field, others send the entire actor object, # so figure out what the actor's URI is based on what we have. - def get_ap_id(object) do - case object do - %{"id" => id} -> id - id -> id - end - end + def get_ap_id(%{"id" => id} = _), do: id + def get_ap_id(id), do: id def normalize_params(params) do Map.put(params, "actor", get_ap_id(params["actor"])) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index d9c1bcb2c..639519e0a 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -31,8 +31,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do def render("endpoints.json", _), do: %{} - # the instance itself is not a Person, but instead an Application - def render("user.json", %{user: %{nickname: nil} = user}) do + def render("service.json", %{user: user}) do {:ok, user} = User.ensure_keys_present(user) {:ok, _, public_key} = Keys.keys_from_pem(user.info.keys) public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key) @@ -47,7 +46,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do "followers" => "#{user.ap_id}/followers", "inbox" => "#{user.ap_id}/inbox", "name" => "Pleroma", - "summary" => "Virtual actor for Pleroma relay", + "summary" => + "An internal service actor for this Pleroma instance. No user-serviceable parts inside.", "url" => user.ap_id, "manuallyApprovesFollowers" => false, "publicKey" => %{ @@ -60,6 +60,13 @@ defmodule Pleroma.Web.ActivityPub.UserView do |> Map.merge(Utils.make_json_ld_header()) end + # the instance itself is not a Person, but instead an Application + def render("user.json", %{user: %User{nickname: nil} = user}), + do: render("service.json", %{user: user}) + + def render("user.json", %{user: %User{nickname: "internal." <> _} = user}), + do: render("service.json", %{user: user}) + def render("user.json", %{user: user}) do {:ok, user} = User.ensure_keys_present(user) {:ok, _, public_key} = Keys.keys_from_pem(user.info.keys) diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 9908a2e75..2666edc7c 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -34,6 +34,20 @@ defmodule Pleroma.Web.ActivityPub.Visibility do !is_public?(activity) && !is_private?(activity) end + def is_list?(%{data: %{"listMessage" => _}}), do: true + def is_list?(_), do: false + + def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true + + def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do + user.ap_id in activity.data["to"] || + list_ap_id + |> Pleroma.List.get_by_ap_id() + |> Pleroma.List.member?(user) + end + + def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false + def visible_for_user?(activity, nil) do is_public?(activity) end @@ -73,6 +87,9 @@ defmodule Pleroma.Web.ActivityPub.Visibility do object.data["directMessage"] == true -> "direct" + is_binary(object.data["listMessage"]) -> + "list" + length(cc) > 0 -> "private" diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index a9164ad98..f4234b743 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Auth.PleromaAuthenticator do - alias Comeonin.Pbkdf2 + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User @@ -16,7 +16,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do def get_user(%Plug.Conn{} = conn) do with {:ok, {name, password}} <- fetch_credentials(conn), {_, %User{} = user} <- {:user, fetch_user(name)}, - {_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do + {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do {:ok, user} else error -> diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index f1450b113..44af6a773 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity - alias Pleroma.Bookmark alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.ThreadMute @@ -31,7 +30,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 @@ -175,6 +175,11 @@ defmodule Pleroma.Web.CommonAPI do when visibility in ~w{public unlisted private direct}, do: {visibility, get_replied_to_visibility(in_reply_to)} + def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do + visibility = {:list, String.to_integer(list_id)} + {visibility, get_replied_to_visibility(in_reply_to)} + end + def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do visibility = get_replied_to_visibility(in_reply_to) {visibility, visibility} @@ -235,19 +240,18 @@ defmodule Pleroma.Web.CommonAPI do "emoji", Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji) ) do - res = - ActivityPub.create( - %{ - to: to, - actor: user, - context: context, - object: object, - additional: %{"cc" => cc, "directMessage" => visibility == "direct"} - }, - Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false - ) - - res + preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false + direct? = visibility == "direct" + + %{ + to: to, + actor: user, + context: context, + object: object, + additional: %{"cc" => cc, "directMessage" => direct?} + } + |> maybe_add_list_data(user, visibility) + |> ActivityPub.create(preview?) else {:private_to_public, true} -> {:error, dgettext("errors", "The message visibility must be direct")} @@ -351,15 +355,6 @@ defmodule Pleroma.Web.CommonAPI do end end - def bookmarked?(user, activity) do - with %Bookmark{} <- Bookmark.get(user.id, activity.id) do - true - else - _ -> - false - end - end - def report(user, data) do with {:account_id, %{"account_id" => account_id}} <- {:account_id, data}, {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)}, diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 8e482eef7..fcc000969 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -6,11 +6,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do import Pleroma.Web.Gettext alias Calendar.Strftime - alias Comeonin.Pbkdf2 alias Pleroma.Activity alias Pleroma.Config alias Pleroma.Formatter alias Pleroma.Object + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils @@ -100,12 +100,29 @@ defmodule Pleroma.Web.CommonAPI.Utils do end end + def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []} + def get_addressed_users(_, to) when is_list(to) do User.get_ap_ids_by_nicknames(to) end def get_addressed_users(mentioned_users, _), do: mentioned_users + def maybe_add_list_data(activity_params, user, {:list, list_id}) do + case Pleroma.List.get(list_id, user) do + %Pleroma.List{} = list -> + activity_params + |> put_in([:additional, "bcc"], [list.ap_id]) + |> put_in([:additional, "listMessage"], list.ap_id) + |> put_in([:object, "listMessage"], list.ap_id) + + _ -> + activity_params + end + end + + def maybe_add_list_data(activity_params, _, _), do: activity_params + def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) when is_list(options) do %{max_expiration: max_expiration, min_expiration: min_expiration} = @@ -371,7 +388,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do def confirm_current_password(user, password) do with %User{local: true} = db_user <- User.get_cached_by_id(user.id), - true <- Pbkdf2.checkpw(password, db_user.password_hash) do + true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do {:ok, db_user} else _ -> {:error, dgettext("errors", "Invalid password.")} 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..877430a1d 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -47,6 +47,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do require Logger + @rate_limited_relations_actions ~w(follow unfollow)a + @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status post_status delete_status)a @@ -62,9 +64,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do when action in ~w(fav_status unfav_status)a ) + plug( + RateLimiter, + {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions + ) + + plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions) 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]) + plug(RateLimiter, :password_reset when action == :password_reset) @local_mastodon_name "Mastodon-Local" @@ -693,11 +702,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - else - {:error, reason} -> - conn - |> put_status(:bad_request) - |> json(%{"error" => reason}) end end @@ -738,11 +742,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - else - {:error, reason} -> - conn - |> put_resp_content_type("application/json") - |> send_resp(:bad_request, Jason.encode!(%{"error" => reason})) end end @@ -881,7 +880,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), + with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id), %Object{data: %{"likes" => likes}} <- Object.normalize(object) do q = from(u in User, where: u.ap_id in ^likes) users = Repo.all(q) @@ -895,7 +894,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), + with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id), %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do q = from(u in User, where: u.ap_id in ^announces) users = Repo.all(q) @@ -1068,9 +1067,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}) @@ -1646,6 +1650,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do render_error(conn, :not_found, "Record not found") end + def errors(conn, {:error, error_message}) do + conn + |> put_status(:bad_request) + |> json(%{error: error_message}) + end + def errors(conn, _) do conn |> put_status(:internal_server_error) @@ -1807,6 +1817,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end + def password_reset(conn, params) do + nickname_or_email = params["email"] || params["nickname"] + + with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do + conn + |> put_status(:no_content) + |> json("") + else + {:error, "unknown user"} -> + send_resp(conn, :not_found, "") + + {:error, _} -> + send_resp(conn, :bad_request, "") + end + end + def try_render(conn, target, params) when is_binary(target) do case render(conn, target, params) do diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 62c516f8e..befb35c26 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -51,8 +51,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do following: User.following?(user, target), followed_by: User.following?(target, user), blocking: User.blocks?(user, target), + blocked_by: User.blocks?(target, user), 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, @@ -136,6 +137,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do |> maybe_put_notification_settings(user, opts[:for]) |> maybe_put_settings_store(user, opts[:for], opts) |> maybe_put_chat_token(user, opts[:for], opts) + |> maybe_put_activation_status(user, opts[:for]) end defp username_from_nickname(string) when is_binary(string) do @@ -196,6 +198,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_notification_settings(data, _, _), do: data + defp maybe_put_activation_status(data, user, %User{info: %{is_admin: true}}) do + Kernel.put_in(data, [:pleroma, :deactivated], user.info.deactivated) + end + + defp maybe_put_activation_status(data, _, _), do: data + defp image_url(%{"url" => [%{"href" => href} | _]}), do: href defp image_url(_), do: nil end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 06a7251d8..de9425959 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -382,7 +382,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do %{ # Mastodon uses separate ids for polls, but an object can't have # more than one poll embedded so object id is fine - id: object.id, + id: to_string(object.id), expires_at: Utils.to_masto_date(end_time), expired: expired, multiple: multiple, 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/controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index ea33d7685..8403850ff 100644 --- a/lib/pleroma/web/media_proxy/controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController 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 + :ok <- filename_matches(params, conn.request_path, url) do ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts)) else false -> @@ -27,13 +27,20 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do end end - def filename_matches(has_filename, path, url) do - filename = url |> MediaProxy.filename() + def filename_matches(%{"filename" => _} = _, path, url) do + filename = MediaProxy.filename(url) - if has_filename && filename && Path.basename(path) != filename do + if filename && does_not_match(path, filename) do {:wrong_filename, filename} else :ok end end + + def filename_matches(_, _, _), do: :ok + + defp does_not_match(path, filename) do + basename = Path.basename(path) + basename != filename and URI.decode(basename) != filename and URI.encode(basename) != filename + end end 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} diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index d1bbebc7d..3d9249601 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -587,7 +587,7 @@ defmodule Pleroma.Web.Router do end end - pipeline :ap_relay do + pipeline :ap_service_actor do plug(:accepts, ["activity+json", "json"]) end @@ -664,8 +664,17 @@ defmodule Pleroma.Web.Router do end scope "/relay", Pleroma.Web.ActivityPub do - pipe_through(:ap_relay) + pipe_through(:ap_service_actor) + get("/", ActivityPubController, :relay) + post("/inbox", ActivityPubController, :inbox) + end + + scope "/internal/fetch", Pleroma.Web.ActivityPub do + pipe_through(:ap_service_actor) + + get("/", ActivityPubController, :internal_fetch) + post("/inbox", ActivityPubController, :inbox) end scope "/", Pleroma.Web.ActivityPub do @@ -692,6 +701,8 @@ defmodule Pleroma.Web.Router do get("/web/login", MastodonAPIController, :login) delete("/auth/sign_out", MastodonAPIController, :logout) + post("/auth/password", MastodonAPIController, :password_reset) + scope [] do pipe_through(:oauth_read_or_public) get("/web/*path", MastodonAPIController, :index) diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index e96e4e1e4..9b01ebcc6 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -123,11 +123,26 @@ defmodule Pleroma.Web.Salmon do {:ok, salmon} end - def remote_users(%{data: %{"to" => to} = data}) do - to = to ++ (data["cc"] || []) + def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do + cc = Map.get(data, "cc", []) + + bcc = + data + |> Map.get("bcc", []) + |> Enum.reduce([], fn ap_id, bcc -> + case Pleroma.List.get_by_ap_id(ap_id) do + %Pleroma.List{user_id: ^user_id} = list -> + {:ok, following} = Pleroma.List.get_following(list) + bcc ++ Enum.map(following, & &1.ap_id) + + _ -> + bcc + end + end) - to - |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end) + [to, cc, bcc] + |> Enum.concat() + |> Enum.map(&User.get_cached_by_ap_id/1) |> Enum.filter(fn user -> user && !user.local end) end @@ -191,7 +206,7 @@ defmodule Pleroma.Web.Salmon do {:ok, private, _} = Keys.keys_from_pem(keys) {:ok, feed} = encode(private, feed) - remote_users = remote_users(activity) + remote_users = remote_users(user, activity) salmon_urls = Enum.map(remote_users, & &1.info.salmon) reachable_urls_metadata = Instances.filter_reachable(salmon_urls) diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index b1863528f..c10c66ff2 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -7,10 +7,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do require Logger - alias Comeonin.Pbkdf2 alias Pleroma.Activity alias Pleroma.Emoji alias Pleroma.Notification + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.User alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub @@ -96,7 +96,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do name = followee.nickname with %User{} = user <- User.get_cached_by_nickname(username), - true <- Pbkdf2.checkpw(password, user.password_hash), + true <- AuthenticationPlug.checkpw(password, user.password_hash), %User{} = _followed <- User.get_cached_by_id(id), {:ok, follower} <- User.follow(user, followee), {:ok, _activity} <- ActivityPub.follow(follower, followee) do diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index 41e1c2877..bb5dda204 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -221,6 +221,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do user |> UserEmail.password_reset_email(token_record.token) |> Mailer.deliver_async() + + {:ok, :enqueued} else false -> {:error, "bad user identifier"} diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 45ef7be3d..5dfab6a6c 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -27,6 +27,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do require Logger + plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset) plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline]) action_fallback(:errors) @@ -192,6 +193,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 @@ -430,6 +438,12 @@ defmodule Pleroma.Web.TwitterAPI.Controller do with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do json_response(conn, :no_content, "") + else + {:error, "unknown user"} -> + send_resp(conn, :not_found, "") + + {:error, _} -> + send_resp(conn, :bad_request, "") end end diff --git a/lib/pleroma/web/uploader_controller.ex b/lib/pleroma/web/uploader_controller.ex index bf09775e6..0cc172698 100644 --- a/lib/pleroma/web/uploader_controller.ex +++ b/lib/pleroma/web/uploader_controller.ex @@ -11,10 +11,6 @@ defmodule Pleroma.Web.UploaderController do process_callback(conn, :global.whereis_name({Uploader, upload_path}), params) end - def callbacks(conn, _) do - render_error(conn, :bad_request, "bad request") - end - defp process_callback(conn, pid, params) when is_pid(pid) do send(pid, {Uploader, self(), conn, params}) diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index 3fca72de8..fa34c7ced 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -32,7 +32,7 @@ defmodule Pleroma.Web.WebFinger do def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do host = Pleroma.Web.Endpoint.host() - regex = ~r/(acct:)?(?<username>\w+)@#{host}/ + regex = ~r/(acct:)?(?<username>[a-z0-9A-Z_\.-]+)@#{host}/ with %{"username" => username} <- Regex.named_captures(regex, resource), %User{} = user <- User.get_cached_by_nickname(username) do |