diff options
Diffstat (limited to 'lib')
45 files changed, 1037 insertions, 632 deletions
diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index 0e21408b2..590c7a914 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -52,7 +52,9 @@ defmodule Mix.Tasks.Pleroma.Config do        |> Enum.each(fn config ->          IO.write(            file, -          "config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n" +          "config :#{config.group}, #{config.key}, #{ +            inspect(Config.from_binary(config.value), limit: :infinity) +          }\r\n\r\n"          )          if delete? do diff --git a/lib/mix/tasks/pleroma/notification_settings.ex b/lib/mix/tasks/pleroma/notification_settings.ex new file mode 100644 index 000000000..7d65f0587 --- /dev/null +++ b/lib/mix/tasks/pleroma/notification_settings.ex @@ -0,0 +1,83 @@ +defmodule Mix.Tasks.Pleroma.NotificationSettings do +  @shortdoc "Enable&Disable privacy option for push notifications" +  @moduledoc """ +  Example: + +  > mix pleroma.notification_settings --privacy-option=false --nickname-users="parallel588"  # set false only for parallel588 user +  > mix pleroma.notification_settings --privacy-option=true # set true for all users + +  """ + +  use Mix.Task +  import Mix.Pleroma +  import Ecto.Query + +  def run(args) do +    start_pleroma() + +    {options, _, _} = +      OptionParser.parse( +        args, +        strict: [ +          privacy_option: :boolean, +          email_users: :string, +          nickname_users: :string +        ] +      ) + +    privacy_option = Keyword.get(options, :privacy_option) + +    if not is_nil(privacy_option) do +      privacy_option +      |> build_query(options) +      |> Pleroma.Repo.update_all([]) +    end + +    shell_info("Done") +  end + +  defp build_query(privacy_option, options) do +    query = +      from(u in Pleroma.User, +        update: [ +          set: [ +            notification_settings: +              fragment( +                "jsonb_set(notification_settings, '{privacy_option}', ?)", +                ^privacy_option +              ) +          ] +        ] +      ) + +    user_emails = +      options +      |> Keyword.get(:email_users, "") +      |> String.split(",") +      |> Enum.map(&String.trim(&1)) +      |> Enum.reject(&(&1 == "")) + +    query = +      if length(user_emails) > 0 do +        where(query, [u], u.email in ^user_emails) +      else +        query +      end + +    user_nicknames = +      options +      |> Keyword.get(:nickname_users, "") +      |> String.split(",") +      |> Enum.map(&String.trim(&1)) +      |> Enum.reject(&(&1 == "")) + +    query = +      if length(user_nicknames) > 0 do +        where(query, [u], u.nickname in ^user_nicknames) +      else +        query +      end + +    query +  end +end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index bc8eacda8..85c9e4954 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -8,7 +8,6 @@ defmodule Mix.Tasks.Pleroma.User do    alias Ecto.Changeset    alias Pleroma.User    alias Pleroma.UserInviteToken -  alias Pleroma.Web.OAuth    @shortdoc "Manages Pleroma users"    @moduledoc File.read!("docs/administration/CLI_tasks/user.md") @@ -354,8 +353,7 @@ defmodule Mix.Tasks.Pleroma.User do      start_pleroma()      with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do -      OAuth.Token.delete_user_tokens(user) -      OAuth.Authorization.delete_user_authorizations(user) +      User.global_sign_out(user)        shell_info("#{nickname} signed out from all apps.")      else @@ -373,9 +371,9 @@ defmodule Mix.Tasks.Pleroma.User do        users        |> Enum.each(fn user ->          shell_info( -          "#{user.nickname} moderator: #{user.info.is_moderator}, admin: #{user.info.is_admin}, locked: #{ -            user.info.locked -          }, deactivated: #{user.info.deactivated}" +          "#{user.nickname} moderator: #{user.is_moderator}, admin: #{user.is_admin}, locked: #{ +            user.locked +          }, deactivated: #{user.deactivated}"          )        end)      end) @@ -393,10 +391,7 @@ defmodule Mix.Tasks.Pleroma.User do    end    defp set_admin(user, value) do -    {:ok, user} = -      user -      |> Changeset.change(%{is_admin: value}) -      |> User.update_and_set_cache() +    {:ok, user} = User.admin_api_update(user, %{is_admin: value})      shell_info("Admin status of #{user.nickname}: #{user.is_admin}")      user diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index cd7a5aae9..480b261cf 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -28,7 +28,8 @@ defmodule Pleroma.Activity do      "Create" => "mention",      "Follow" => "follow",      "Announce" => "reblog", -    "Like" => "favourite" +    "Like" => "favourite", +    "Move" => "move"    }    @mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types, @@ -240,9 +241,10 @@ defmodule Pleroma.Activity do    def normalize(ap_id) when is_binary(ap_id), do: get_by_ap_id_with_object(ap_id)    def normalize(_), do: nil -  def delete_by_ap_id(id) when is_binary(id) do +  def delete_all_by_object_ap_id(id) when is_binary(id) do      id      |> Queries.by_object_id() +    |> Queries.exclude_type("Delete")      |> select([u], u)      |> Repo.delete_all()      |> elem(1) @@ -254,7 +256,7 @@ defmodule Pleroma.Activity do      |> purge_web_resp_cache()    end -  def delete_by_ap_id(_), do: nil +  def delete_all_by_object_ap_id(_), do: nil    defp purge_web_resp_cache(%Activity{} = activity) do      %{path: path} = URI.parse(activity.data["id"]) diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex index 949f010a8..26bc1099d 100644 --- a/lib/pleroma/activity/queries.ex +++ b/lib/pleroma/activity/queries.ex @@ -64,4 +64,12 @@ defmodule Pleroma.Activity.Queries do        where: fragment("(?)->>'type' = ?", activity.data, ^activity_type)      )    end + +  @spec exclude_type(query, String.t()) :: query +  def exclude_type(query \\ Activity, activity_type) do +    from( +      activity in query, +      where: fragment("(?)->>'type' != ?", activity.data, ^activity_type) +    ) +  end  end diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index f847ac238..d30a5a6a5 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -86,7 +86,7 @@ defmodule Pleroma.Activity.Search do           {:ok, object} <- Fetcher.fetch_object_from_id(search_query),           %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),           true <- Visibility.visible_for_user?(activity, user) do -      activities ++ [activity] +      [activity | activities]      else        _ -> activities      end diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 9dbd1e26b..5b844aa41 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -30,6 +30,7 @@ defmodule Pleroma.Application do    # See http://elixir-lang.org/docs/stable/elixir/Application.html    # for more information on OTP Applications    def start(_type, _args) do +    Pleroma.HTML.compile_scrubbers()      Pleroma.Config.DeprecationWarnings.warn()      setup_instrumenters() @@ -147,8 +148,6 @@ defmodule Pleroma.Application do    defp oauth_cleanup_child(_), do: [] -  defp chat_child(:test, _), do: [] -    defp chat_child(_env, true) do      [Pleroma.Web.ChatChannel.ChatChannelState]    end diff --git a/lib/pleroma/clippy.ex b/lib/pleroma/clippy.ex index bd20952a6..6e6121d4e 100644 --- a/lib/pleroma/clippy.ex +++ b/lib/pleroma/clippy.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Clippy do    @moduledoc false +    # No software is complete until they have a Clippy implementation.    # A ballmer peak _may_ be required to change this module. diff --git a/lib/pleroma/config.ex b/lib/pleroma/config.ex index fcc039710..cadab2f15 100644 --- a/lib/pleroma/config.ex +++ b/lib/pleroma/config.ex @@ -65,4 +65,11 @@ defmodule Pleroma.Config do    def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])    def oauth_consumer_enabled?, do: oauth_consumer_strategies() != [] + +  def enforce_oauth_admin_scope_usage?, do: !!get([:auth, :enforce_oauth_admin_scope_usage]) + +  def oauth_admin_scopes(scope) do +    ["admin:#{scope}"] ++ +      if enforce_oauth_admin_scope_usage?(), do: [], else: [scope] +  end  end diff --git a/lib/pleroma/ecto_enums.ex b/lib/pleroma/ecto_enums.ex new file mode 100644 index 000000000..b86229312 --- /dev/null +++ b/lib/pleroma/ecto_enums.ex @@ -0,0 +1,13 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +import EctoEnum + +defenum(UserRelationshipTypeEnum, +  block: 1, +  mute: 2, +  reblog_mute: 3, +  notification_mute: 4, +  inverse_subscription: 5 +) diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 3aff9fb76..0b0219b82 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -107,4 +107,26 @@ defmodule Pleroma.FollowingRelationship do        [user.follower_address | following]      end    end + +  def move_following(origin, target) do +    __MODULE__ +    |> join(:inner, [r], f in assoc(r, :follower)) +    |> where(following_id: ^origin.id) +    |> where([r, f], f.allow_following_move == true) +    |> limit(50) +    |> preload([:follower]) +    |> Repo.all() +    |> Enum.map(fn following_relationship -> +      Repo.delete(following_relationship) +      Pleroma.Web.CommonAPI.follow(following_relationship.follower, target) +    end) +    |> case do +      [] -> +        User.update_follower_count(origin) +        :ok + +      _ -> +        move_following(origin, target) +    end +  end  end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index 997e965f0..2cae29f35 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -3,6 +3,25 @@  # SPDX-License-Identifier: AGPL-3.0-only  defmodule Pleroma.HTML do +  # Scrubbers are compiled on boot so they can be configured in OTP releases +  #  @on_load :compile_scrubbers + +  def compile_scrubbers do +    dir = Path.join(:code.priv_dir(:pleroma), "scrubbers") + +    dir +    |> File.ls!() +    |> Enum.map(&Path.join(dir, &1)) +    |> Kernel.ParallelCompiler.compile() +    |> case do +      {:error, _errors, _warnings} -> +        raise "Compiling scrubbers failed" + +      {:ok, _modules, _warnings} -> +        :ok +    end +  end +    defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber]    defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers    defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default] @@ -99,215 +118,3 @@ defmodule Pleroma.HTML do      end)    end  end - -defmodule Pleroma.HTML.Scrubber.TwitterText do -  @moduledoc """ -  An HTML scrubbing policy which limits to twitter-style text.  Only -  paragraphs, breaks and links are allowed through the filter. -  """ - -  @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - -  require FastSanitize.Sanitizer.Meta -  alias FastSanitize.Sanitizer.Meta - -  Meta.strip_comments() - -  # links -  Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) - -  Meta.allow_tag_with_this_attribute_values(:a, "class", [ -    "hashtag", -    "u-url", -    "mention", -    "u-url mention", -    "mention u-url" -  ]) - -  Meta.allow_tag_with_this_attribute_values(:a, "rel", [ -    "tag", -    "nofollow", -    "noopener", -    "noreferrer" -  ]) - -  Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) - -  # paragraphs and linebreaks -  Meta.allow_tag_with_these_attributes(:br, []) -  Meta.allow_tag_with_these_attributes(:p, []) - -  # microformats -  Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"]) -  Meta.allow_tag_with_these_attributes(:span, []) - -  # allow inline images for custom emoji -  if Pleroma.Config.get([:markup, :allow_inline_images]) do -    # restrict img tags to http/https only, because of MediaProxy. -    Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) - -    Meta.allow_tag_with_these_attributes(:img, [ -      "width", -      "height", -      "class", -      "title", -      "alt" -    ]) -  end - -  Meta.strip_everything_not_covered() -end - -defmodule Pleroma.HTML.Scrubber.Default do -  @doc "The default HTML scrubbing policy: no " - -  require FastSanitize.Sanitizer.Meta -  alias FastSanitize.Sanitizer.Meta -  # credo:disable-for-previous-line -  # No idea how to fix this one… - -  @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - -  Meta.strip_comments() - -  Meta.allow_tag_with_uri_attributes(:a, ["href", "data-user", "data-tag"], @valid_schemes) - -  Meta.allow_tag_with_this_attribute_values(:a, "class", [ -    "hashtag", -    "u-url", -    "mention", -    "u-url mention", -    "mention u-url" -  ]) - -  Meta.allow_tag_with_this_attribute_values(:a, "rel", [ -    "tag", -    "nofollow", -    "noopener", -    "noreferrer", -    "ugc" -  ]) - -  Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) - -  Meta.allow_tag_with_these_attributes(:abbr, ["title"]) - -  Meta.allow_tag_with_these_attributes(:b, []) -  Meta.allow_tag_with_these_attributes(:blockquote, []) -  Meta.allow_tag_with_these_attributes(:br, []) -  Meta.allow_tag_with_these_attributes(:code, []) -  Meta.allow_tag_with_these_attributes(:del, []) -  Meta.allow_tag_with_these_attributes(:em, []) -  Meta.allow_tag_with_these_attributes(:i, []) -  Meta.allow_tag_with_these_attributes(:li, []) -  Meta.allow_tag_with_these_attributes(:ol, []) -  Meta.allow_tag_with_these_attributes(:p, []) -  Meta.allow_tag_with_these_attributes(:pre, []) -  Meta.allow_tag_with_these_attributes(:strong, []) -  Meta.allow_tag_with_these_attributes(:sub, []) -  Meta.allow_tag_with_these_attributes(:sup, []) -  Meta.allow_tag_with_these_attributes(:u, []) -  Meta.allow_tag_with_these_attributes(:ul, []) - -  Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card"]) -  Meta.allow_tag_with_these_attributes(:span, []) - -  @allow_inline_images Pleroma.Config.get([:markup, :allow_inline_images]) - -  if @allow_inline_images do -    # restrict img tags to http/https only, because of MediaProxy. -    Meta.allow_tag_with_uri_attributes(:img, ["src"], ["http", "https"]) - -    Meta.allow_tag_with_these_attributes(:img, [ -      "width", -      "height", -      "class", -      "title", -      "alt" -    ]) -  end - -  if Pleroma.Config.get([:markup, :allow_tables]) do -    Meta.allow_tag_with_these_attributes(:table, []) -    Meta.allow_tag_with_these_attributes(:tbody, []) -    Meta.allow_tag_with_these_attributes(:td, []) -    Meta.allow_tag_with_these_attributes(:th, []) -    Meta.allow_tag_with_these_attributes(:thead, []) -    Meta.allow_tag_with_these_attributes(:tr, []) -  end - -  if Pleroma.Config.get([:markup, :allow_headings]) do -    Meta.allow_tag_with_these_attributes(:h1, []) -    Meta.allow_tag_with_these_attributes(:h2, []) -    Meta.allow_tag_with_these_attributes(:h3, []) -    Meta.allow_tag_with_these_attributes(:h4, []) -    Meta.allow_tag_with_these_attributes(:h5, []) -  end - -  if Pleroma.Config.get([:markup, :allow_fonts]) do -    Meta.allow_tag_with_these_attributes(:font, ["face"]) -  end - -  Meta.strip_everything_not_covered() -end - -defmodule Pleroma.HTML.Transform.MediaProxy do -  @moduledoc "Transforms inline image URIs to use MediaProxy." - -  alias Pleroma.Web.MediaProxy - -  def before_scrub(html), do: html - -  def scrub_attribute(:img, {"src", "http" <> target}) do -    media_url = -      ("http" <> target) -      |> MediaProxy.url() - -    {"src", media_url} -  end - -  def scrub_attribute(_tag, attribute), do: attribute - -  def scrub({:img, attributes, children}) do -    attributes = -      attributes -      |> Enum.map(fn attr -> scrub_attribute(:img, attr) end) -      |> Enum.reject(&is_nil(&1)) - -    {:img, attributes, children} -  end - -  def scrub({:comment, _text, _children}), do: "" - -  def scrub({tag, attributes, children}), do: {tag, attributes, children} -  def scrub({_tag, children}), do: children -  def scrub(text), do: text -end - -defmodule Pleroma.HTML.Scrubber.LinksOnly do -  @moduledoc """ -  An HTML scrubbing policy which limits to links only. -  """ - -  @valid_schemes Pleroma.Config.get([:uri_schemes, :valid_schemes], []) - -  require FastSanitize.Sanitizer.Meta -  alias FastSanitize.Sanitizer.Meta - -  Meta.strip_comments() - -  # links -  Meta.allow_tag_with_uri_attributes(:a, ["href"], @valid_schemes) - -  Meta.allow_tag_with_this_attribute_values(:a, "rel", [ -    "tag", -    "nofollow", -    "noopener", -    "noreferrer", -    "me", -    "ugc" -  ]) - -  Meta.allow_tag_with_these_attributes(:a, ["name", "title"]) -  Meta.strip_everything_not_covered() -end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index b7ecf51e4..8f3e46af9 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -21,6 +21,8 @@ defmodule Pleroma.Notification do    @type t :: %__MODULE__{} +  @include_muted_option :with_muted +    schema "notifications" do      field(:seen, :boolean, default: false)      belongs_to(:user, User, type: FlakeId.Ecto.CompatType) @@ -34,7 +36,25 @@ defmodule Pleroma.Notification do      |> cast(attrs, [:seen])    end -  def for_user_query(user, opts \\ []) do +  defp for_user_query_ap_id_opts(user, opts) do +    ap_id_relations = +      [:block] ++ +        if opts[@include_muted_option], do: [], else: [:notification_mute] + +    preloaded_ap_ids = User.outgoing_relations_ap_ids(user, ap_id_relations) + +    exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts) + +    exclude_notification_muted_opts = +      Map.merge(%{notification_muted_users_ap_ids: preloaded_ap_ids[:notification_mute]}, opts) + +    {exclude_blocked_opts, exclude_notification_muted_opts} +  end + +  def for_user_query(user, opts \\ %{}) do +    {exclude_blocked_opts, exclude_notification_muted_opts} = +      for_user_query_ap_id_opts(user, opts) +      Notification      |> where(user_id: ^user.id)      |> where( @@ -54,43 +74,75 @@ defmodule Pleroma.Notification do          )      )      |> preload([n, a, o], activity: {a, object: o}) -    |> exclude_muted(user, opts) -    |> exclude_blocked(user) +    |> exclude_notification_muted(user, exclude_notification_muted_opts) +    |> exclude_blocked(user, exclude_blocked_opts)      |> exclude_visibility(opts) +    |> exclude_move(opts)    end -  defp exclude_blocked(query, user) do +  defp exclude_blocked(query, user, opts) do +    blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user) +      query -    |> where([n, a], a.actor not in ^user.blocks) +    |> where([n, a], a.actor not in ^blocked_ap_ids)      |> where(        [n, a],        fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks      )    end -  defp exclude_muted(query, _, %{with_muted: true}) do +  defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do      query    end -  defp exclude_muted(query, user, _opts) do +  defp exclude_notification_muted(query, user, opts) do +    notification_muted_ap_ids = +      opts[:notification_muted_users_ap_ids] || User.notification_muted_users_ap_ids(user) +      query -    |> where([n, a], a.actor not in ^user.muted_notifications) +    |> where([n, a], a.actor not in ^notification_muted_ap_ids)      |> 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 +  defp exclude_move(query, %{with_move: true}) do +    query +  end + +  defp exclude_move(query, _opts) do +    where(query, [n, a], fragment("?->>'type' != 'Move'", a.data)) +  end +    @valid_visibilities ~w[direct unlisted public private]    defp exclude_visibility(query, %{exclude_visibilities: visibility})         when is_list(visibility) do      if Enum.all?(visibility, &(&1 in @valid_visibilities)) do        query +      |> join(:left, [n, a], mutated_activity in Pleroma.Activity, +        on: +          fragment("?->>'context'", a.data) == +            fragment("?->>'context'", mutated_activity.data) and +            fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and +            fragment("?->>'type'", mutated_activity.data) == "Create", +        as: :mutated_activity +      )        |> where( -        [n, a], +        [n, a, mutated_activity: mutated_activity],          not fragment( -          "activity_visibility(?, ?, ?) = ANY (?)", +          """ +          CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce' +            THEN (activity_visibility(?, ?, ?) = ANY (?)) +            ELSE (activity_visibility(?, ?, ?) = ANY (?)) END +          """, +          a.data, +          a.data, +          mutated_activity.actor, +          mutated_activity.recipients, +          mutated_activity.data, +          ^visibility,            a.actor,            a.recipients,            a.data, @@ -105,17 +157,7 @@ defmodule Pleroma.Notification do    defp exclude_visibility(query, %{exclude_visibilities: visibility})         when visibility in @valid_visibilities do -    query -    |> where( -      [n, a], -      not fragment( -        "activity_visibility(?, ?, ?) = (?)", -        a.actor, -        a.recipients, -        a.data, -        ^visibility -      ) -    ) +    exclude_visibility(query, [visibility])    end    defp exclude_visibility(query, %{exclude_visibilities: visibility}) @@ -251,10 +293,13 @@ defmodule Pleroma.Notification do      end    end -  def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity) -      when type in ["Like", "Announce", "Follow"] do -    users = get_notified_from_activity(activity) -    notifications = Enum.map(users, fn user -> create_notification(activity, user) end) +  def create_notifications(%Activity{data: %{"type" => type}} = activity) +      when type in ["Like", "Announce", "Follow", "Move"] do +    notifications = +      activity +      |> get_notified_from_activity() +      |> Enum.map(&create_notification(activity, &1)) +      {:ok, notifications}    end @@ -276,19 +321,15 @@ defmodule Pleroma.Notification do    def get_notified_from_activity(activity, local_only \\ true) -  def get_notified_from_activity( -        %Activity{data: %{"to" => _, "type" => type} = _data} = activity, -        local_only -      ) -      when type in ["Create", "Like", "Announce", "Follow"] do -    recipients = -      [] -      |> Utils.maybe_notify_to_recipients(activity) -      |> Utils.maybe_notify_mentioned_recipients(activity) -      |> Utils.maybe_notify_subscribers(activity) -      |> Enum.uniq() - -    User.get_users_from_set(recipients, local_only) +  def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) +      when type in ["Create", "Like", "Announce", "Follow", "Move"] do +    [] +    |> Utils.maybe_notify_to_recipients(activity) +    |> Utils.maybe_notify_mentioned_recipients(activity) +    |> Utils.maybe_notify_subscribers(activity) +    |> Utils.maybe_notify_followers(activity) +    |> Enum.uniq() +    |> User.get_users_from_set(local_only)    end    def get_notified_from_activity(_, _local_only), do: [] @@ -314,7 +355,7 @@ defmodule Pleroma.Notification do    def skip?(          :followers,          activity, -        %{notification_settings: %{"followers" => false}} = user +        %{notification_settings: %{followers: false}} = user        ) do      actor = activity.data["actor"]      follower = User.get_cached_by_ap_id(actor) @@ -324,14 +365,14 @@ defmodule Pleroma.Notification do    def skip?(          :non_followers,          activity, -        %{notification_settings: %{"non_followers" => false}} = user +        %{notification_settings: %{non_followers: false}} = user        ) do      actor = activity.data["actor"]      follower = User.get_cached_by_ap_id(actor)      !User.following?(follower, user)    end -  def skip?(:follows, activity, %{notification_settings: %{"follows" => false}} = user) do +  def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user) do      actor = activity.data["actor"]      followed = User.get_cached_by_ap_id(actor)      User.following?(user, followed) @@ -340,7 +381,7 @@ defmodule Pleroma.Notification do    def skip?(          :non_follows,          activity, -        %{notification_settings: %{"non_follows" => false}} = user +        %{notification_settings: %{non_follows: false}} = user        ) do      actor = activity.data["actor"]      followed = User.get_cached_by_ap_id(actor) diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index b4ed3a9b2..ff0e59241 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -147,7 +147,7 @@ defmodule Pleroma.Object do    def delete(%Object{data: %{"id" => id}} = object) do      with {:ok, _obj} = swap_object_with_tombstone(object), -         deleted_activity = Activity.delete_by_ap_id(id), +         deleted_activity = Activity.delete_all_by_object_ap_id(id),           {:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),           {:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do        {:ok, object, deleted_activity} diff --git a/lib/pleroma/plugs/oauth_scopes_plug.ex b/lib/pleroma/plugs/oauth_scopes_plug.ex index a3278dbef..3201fb399 100644 --- a/lib/pleroma/plugs/oauth_scopes_plug.ex +++ b/lib/pleroma/plugs/oauth_scopes_plug.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do    import Plug.Conn    import Pleroma.Web.Gettext +  alias Pleroma.Config    alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug    @behaviour Plug @@ -15,6 +16,14 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do    def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do      op = options[:op] || :|      token = assigns[:token] + +    scopes = +      if options[:admin] do +        Config.oauth_admin_scopes(scopes) +      else +        scopes +      end +      matched_scopes = token && filter_descendants(scopes, token.scopes)      cond do diff --git a/lib/pleroma/plugs/parsers_plug.ex b/lib/pleroma/plugs/parsers_plug.ex new file mode 100644 index 000000000..2e493ce0e --- /dev/null +++ b/lib/pleroma/plugs/parsers_plug.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.Parsers do +  @moduledoc "Initializes Plug.Parsers with upload limit set at boot time" + +  @behaviour Plug + +  def init(_opts) do +    Plug.Parsers.init( +      parsers: [:urlencoded, :multipart, :json], +      pass: ["*/*"], +      json_decoder: Jason, +      length: Pleroma.Config.get([:instance, :upload_limit]), +      body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} +    ) +  end + +  defdelegate call(conn, opts), to: Plug.Parsers +end diff --git a/lib/pleroma/plugs/user_is_admin_plug.ex b/lib/pleroma/plugs/user_is_admin_plug.ex index ee808f31f..582fb1f92 100644 --- a/lib/pleroma/plugs/user_is_admin_plug.ex +++ b/lib/pleroma/plugs/user_is_admin_plug.ex @@ -5,19 +5,38 @@  defmodule Pleroma.Plugs.UserIsAdminPlug do    import Pleroma.Web.TranslationHelpers    import Plug.Conn +    alias Pleroma.User +  alias Pleroma.Web.OAuth    def init(options) do      options    end -  def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do -    conn +  def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do +    token = assigns[:token] + +    cond do +      not Pleroma.Config.enforce_oauth_admin_scope_usage?() -> +        conn + +      token && OAuth.Scopes.contains_admin_scopes?(token.scopes) -> +        # Note: checking for _any_ admin scope presence, not necessarily fitting requested action. +        #   Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements. +        conn + +      true -> +        fail(conn) +    end    end    def call(conn, _) do +    fail(conn) +  end + +  defp fail(conn) do      conn -    |> render_error(:forbidden, "User is not admin.") -    |> halt +    |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.") +    |> halt()    end  end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 601aa9cf0..cebadc8ab 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -7,6 +7,7 @@ defmodule Pleroma.User do    import Ecto.Changeset    import Ecto.Query +  import Ecto, only: [assoc: 2]    alias Comeonin.Pbkdf2    alias Ecto.Multi @@ -21,6 +22,7 @@ defmodule Pleroma.User do    alias Pleroma.Repo    alias Pleroma.RepoStreamer    alias Pleroma.User +  alias Pleroma.UserRelationship    alias Pleroma.Web    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils @@ -42,6 +44,32 @@ defmodule Pleroma.User do    @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/    @extended_local_nickname_regex ~r/^[a-zA-Z\d_-]+$/ +  # AP ID user relationships (blocks, mutes etc.) +  # Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]] +  @user_relationships_config [ +    block: [ +      blocker_blocks: :blocked_users, +      blockee_blocks: :blocker_users +    ], +    mute: [ +      muter_mutes: :muted_users, +      mutee_mutes: :muter_users +    ], +    reblog_mute: [ +      reblog_muter_mutes: :reblog_muted_users, +      reblog_mutee_mutes: :reblog_muter_users +    ], +    notification_mute: [ +      notification_muter_mutes: :notification_muted_users, +      notification_mutee_mutes: :notification_muter_users +    ], +    # Note: `inverse_subscription` relationship is inverse: subscriber acts as relationship target +    inverse_subscription: [ +      subscribee_subscriptions: :subscriber_users, +      subscriber_subscriptions: :subscribee_users +    ] +  ] +    schema "users" do      field(:bio, :string)      field(:email, :string) @@ -61,7 +89,6 @@ defmodule Pleroma.User do      field(:tags, {:array, :string}, default: [])      field(:last_refreshed_at, :naive_datetime_usec)      field(:last_digest_emailed_at, :naive_datetime) -      field(:banner, :map, default: %{})      field(:background, :map, default: %{})      field(:source_data, :map, default: %{}) @@ -73,12 +100,7 @@ defmodule Pleroma.User do      field(:password_reset_pending, :boolean, default: false)      field(:confirmation_token, :string, default: nil)      field(:default_scope, :string, default: "public") -    field(:blocks, {:array, :string}, default: [])      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)      field(:ap_enabled, :boolean, default: false) @@ -103,26 +125,97 @@ defmodule Pleroma.User do      field(:raw_fields, {:array, :map}, default: [])      field(:discoverable, :boolean, default: false)      field(:invisible, :boolean, default: false) +    field(:allow_following_move, :boolean, default: true)      field(:skip_thread_containment, :boolean, default: false) +    field(:actor_type, :string, default: "Person") +    field(:also_known_as, {:array, :string}, default: []) -    field(:notification_settings, :map, -      default: %{ -        "followers" => true, -        "follows" => true, -        "non_follows" => true, -        "non_followers" => true -      } +    embeds_one( +      :notification_settings, +      Pleroma.User.NotificationSetting, +      on_replace: :update      )      has_many(:notifications, Notification)      has_many(:registrations, Registration)      has_many(:deliveries, Delivery) -    field(:info, :map, default: %{}) +    has_many(:outgoing_relationships, UserRelationship, foreign_key: :source_id) +    has_many(:incoming_relationships, UserRelationship, foreign_key: :target_id) + +    for {relationship_type, +         [ +           {outgoing_relation, outgoing_relation_target}, +           {incoming_relation, incoming_relation_source} +         ]} <- @user_relationships_config do +      # Definitions of `has_many :blocker_blocks`, `has_many :muter_mutes` etc. +      has_many(outgoing_relation, UserRelationship, +        foreign_key: :source_id, +        where: [relationship_type: relationship_type] +      ) + +      # Definitions of `has_many :blockee_blocks`, `has_many :mutee_mutes` etc. +      has_many(incoming_relation, UserRelationship, +        foreign_key: :target_id, +        where: [relationship_type: relationship_type] +      ) + +      # Definitions of `has_many :blocked_users`, `has_many :muted_users` etc. +      has_many(outgoing_relation_target, through: [outgoing_relation, :target]) + +      # Definitions of `has_many :blocker_users`, `has_many :muter_users` etc. +      has_many(incoming_relation_source, through: [incoming_relation, :source]) +    end + +    # `:blocks` is deprecated (replaced with `blocked_users` relation) +    field(:blocks, {:array, :string}, default: []) +    # `:mutes` is deprecated (replaced with `muted_users` relation) +    field(:mutes, {:array, :string}, default: []) +    # `:muted_reblogs` is deprecated (replaced with `reblog_muted_users` relation) +    field(:muted_reblogs, {:array, :string}, default: []) +    # `:muted_notifications` is deprecated (replaced with `notification_muted_users` relation) +    field(:muted_notifications, {:array, :string}, default: []) +    # `:subscribers` is deprecated (replaced with `subscriber_users` relation) +    field(:subscribers, {:array, :string}, default: [])      timestamps()    end +  for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <- +        @user_relationships_config do +    # Definitions of `blocked_users_relation/1`, `muted_users_relation/1`, etc. +    def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do +      target_users_query = assoc(user, unquote(outgoing_relation_target)) + +      if restrict_deactivated? do +        restrict_deactivated(target_users_query) +      else +        target_users_query +      end +    end + +    # Definitions of `blocked_users/1`, `muted_users/1`, etc. +    def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do +      __MODULE__ +      |> apply(unquote(:"#{outgoing_relation_target}_relation"), [ +        user, +        restrict_deactivated? +      ]) +      |> Repo.all() +    end + +    # Definitions of `blocked_users_ap_ids/1`, `muted_users_ap_ids/1`, etc. +    def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do +      __MODULE__ +      |> apply(unquote(:"#{outgoing_relation_target}_relation"), [ +        user, +        restrict_deactivated? +      ]) +      |> select([u], u.ap_id) +      |> Repo.all() +    end +  end +    @doc "Returns if the user should be allowed to authenticate"    def auth_active?(%User{deactivated: true}), do: false @@ -225,7 +318,6 @@ defmodule Pleroma.User do      params =        params -      |> Map.put(:info, params[:info] || %{})        |> truncate_if_exists(:name, name_limit)        |> truncate_if_exists(:bio, bio_limit)        |> truncate_fields_param() @@ -254,7 +346,9 @@ defmodule Pleroma.User do            :fields,            :following_count,            :discoverable, -          :invisible +          :invisible, +          :actor_type, +          :also_known_as          ]        )        |> validate_required([:name, :ap_id]) @@ -296,13 +390,16 @@ defmodule Pleroma.User do          :hide_followers_count,          :hide_follows_count,          :hide_favorites, +        :allow_following_move,          :background,          :show_role,          :skip_thread_containment,          :fields,          :raw_fields,          :pleroma_settings_store, -        :discoverable +        :discoverable, +        :actor_type, +        :also_known_as        ]      )      |> unique_constraint(:nickname) @@ -340,9 +437,12 @@ defmodule Pleroma.User do          :hide_follows,          :fields,          :hide_followers, +        :allow_following_move,          :discoverable,          :hide_followers_count, -        :hide_follows_count +        :hide_follows_count, +        :actor_type, +        :also_known_as        ]      )      |> unique_constraint(:nickname) @@ -949,34 +1049,45 @@ defmodule Pleroma.User do      |> Repo.all()    end -  @spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()} -  def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do -    add_to_mutes(muter, ap_id, notifications?) +  @spec mute(User.t(), User.t(), boolean()) :: +          {:ok, list(UserRelationship.t())} | {:error, String.t()} +  def mute(%User{} = muter, %User{} = mutee, notifications? \\ true) do +    add_to_mutes(muter, mutee, notifications?)    end -  def unmute(muter, %{ap_id: ap_id}) do -    remove_from_mutes(muter, ap_id) +  def unmute(%User{} = muter, %User{} = mutee) do +    remove_from_mutes(muter, mutee)    end -  def subscribe(subscriber, %{ap_id: ap_id}) do -    with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do -      deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) +  def subscribe(%User{} = subscriber, %User{} = target) do +    deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) -      if blocks?(subscribed, subscriber) and deny_follow_blocked do -        {:error, "Could not subscribe: #{subscribed.nickname} is blocking you"} -      else -        User.add_to_subscribers(subscribed, subscriber.ap_id) -      end +    if blocks?(target, subscriber) and deny_follow_blocked do +      {:error, "Could not subscribe: #{target.nickname} is blocking you"} +    else +      # Note: the relationship is inverse: subscriber acts as relationship target +      UserRelationship.create_inverse_subscription(target, subscriber)      end    end -  def unsubscribe(unsubscriber, %{ap_id: ap_id}) do +  def subscribe(%User{} = subscriber, %{ap_id: ap_id}) do +    with %User{} = subscribee <- get_cached_by_ap_id(ap_id) do +      subscribe(subscriber, subscribee) +    end +  end + +  def unsubscribe(%User{} = unsubscriber, %User{} = target) do +    # Note: the relationship is inverse: subscriber acts as relationship target +    UserRelationship.delete_inverse_subscription(target, unsubscriber) +  end + +  def unsubscribe(%User{} = unsubscriber, %{ap_id: ap_id}) do      with %User{} = user <- get_cached_by_ap_id(ap_id) do -      User.remove_from_subscribers(user, unsubscriber.ap_id) +      unsubscribe(unsubscriber, user)      end    end -  def block(blocker, %User{ap_id: ap_id} = blocked) do +  def block(%User{} = blocker, %User{} = blocked) do      # sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)      blocker =        if following?(blocker, blocked) do @@ -993,51 +1104,54 @@ defmodule Pleroma.User do          nil -> blocked        end -    blocker = -      if subscribed_to?(blocked, blocker) do -        {:ok, blocker} = unsubscribe(blocked, blocker) -        blocker -      else -        blocker -      end +    unsubscribe(blocked, blocker)      if following?(blocked, blocker), do: unfollow(blocked, blocker)      {:ok, blocker} = update_follower_count(blocker)      {:ok, blocker, _} = Participation.mark_all_as_read(blocker, blocked) -    add_to_block(blocker, ap_id) +    add_to_block(blocker, blocked)    end    # helper to handle the block given only an actor's AP id -  def block(blocker, %{ap_id: ap_id}) do +  def block(%User{} = blocker, %{ap_id: ap_id}) do      block(blocker, get_cached_by_ap_id(ap_id))    end -  def unblock(blocker, %{ap_id: ap_id}) do -    remove_from_block(blocker, ap_id) +  def unblock(%User{} = blocker, %User{} = blocked) do +    remove_from_block(blocker, blocked) +  end + +  # helper to handle the block given only an actor's AP id +  def unblock(%User{} = blocker, %{ap_id: ap_id}) do +    unblock(blocker, get_cached_by_ap_id(ap_id))    end    def mutes?(nil, _), do: false -  def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.mutes, ap_id) +  def mutes?(%User{} = user, %User{} = target), do: mutes_user?(user, target) + +  def mutes_user?(%User{} = user, %User{} = target) do +    UserRelationship.mute_exists?(user, target) +  end    @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.muted_notifications, ap_id) +  def muted_notifications?(%User{} = user, %User{} = target), +    do: UserRelationship.notification_mute_exists?(user, target) + +  def blocks?(nil, _), do: false    def blocks?(%User{} = user, %User{} = target) do      blocks_ap_id?(user, target) ||        (!User.following?(user, target) && blocks_domain?(user, target))    end -  def blocks?(nil, _), do: false - -  def blocks_ap_id?(%User{} = user, %User{} = target) do -    Enum.member?(user.blocks, target.ap_id) +  def blocks_user?(%User{} = user, %User{} = target) do +    UserRelationship.block_exists?(user, target)    end -  def blocks_ap_id?(_, _), do: false +  def blocks_user?(_, _), do: false    def blocks_domain?(%User{} = user, %User{} = target) do      domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks) @@ -1047,28 +1161,41 @@ defmodule Pleroma.User do    def blocks_domain?(_, _), do: false -  def subscribed_to?(user, %{ap_id: ap_id}) do +  def subscribed_to?(%User{} = user, %User{} = target) do +    # Note: the relationship is inverse: subscriber acts as relationship target +    UserRelationship.inverse_subscription_exists?(target, user) +  end + +  def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do      with %User{} = target <- get_cached_by_ap_id(ap_id) do -      Enum.member?(target.subscribers, user.ap_id) +      subscribed_to?(user, target)      end    end -  @spec muted_users(User.t()) :: [User.t()] -  def muted_users(user) do -    User.Query.build(%{ap_id: user.mutes, deactivated: false}) -    |> Repo.all() -  end +  @doc """ +  Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type. +  E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}` +  """ +  @spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())} +  def outgoing_relations_ap_ids(_, []), do: %{} -  @spec blocked_users(User.t()) :: [User.t()] -  def blocked_users(user) do -    User.Query.build(%{ap_id: user.blocks, deactivated: false}) -    |> Repo.all() -  end +  def outgoing_relations_ap_ids(%User{} = user, relationship_types) +      when is_list(relationship_types) do +    db_result = +      user +      |> assoc(:outgoing_relationships) +      |> join(:inner, [user_rel], u in assoc(user_rel, :target)) +      |> where([user_rel, u], user_rel.relationship_type in ^relationship_types) +      |> select([user_rel, u], [user_rel.relationship_type, fragment("array_agg(?)", u.ap_id)]) +      |> group_by([user_rel, u], user_rel.relationship_type) +      |> Repo.all() +      |> Enum.into(%{}, fn [k, v] -> {k, v} end) -  @spec subscribers(User.t()) :: [User.t()] -  def subscribers(user) do -    User.Query.build(%{ap_id: user.subscribers, deactivated: false}) -    |> Repo.all() +    Enum.into( +      relationship_types, +      %{}, +      fn rel_type -> {rel_type, db_result[rel_type] || []} end +    )    end    def deactivate_async(user, status \\ true) do @@ -1103,20 +1230,9 @@ defmodule Pleroma.User do    end    def update_notification_settings(%User{} = user, settings) do -    settings = -      settings -      |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end) -      |> Map.new() - -    notification_settings = -      user.notification_settings -      |> Map.merge(settings) -      |> Map.take(["followers", "follows", "non_follows", "non_followers"]) - -    params = %{notification_settings: notification_settings} -      user -    |> cast(params, [:notification_settings]) +    |> cast(%{notification_settings: settings}, []) +    |> cast_embed(:notification_settings)      |> validate_required([:notification_settings])      |> update_and_set_cache()    end @@ -1175,7 +1291,7 @@ defmodule Pleroma.User do        blocked_identifiers,        fn blocked_identifier ->          with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), -             {:ok, blocker} <- block(blocker, blocked), +             {:ok, _user_block} <- block(blocker, blocked),               {:ok, _} <- ActivityPub.block(blocker, blocked) do            blocked          else @@ -1219,7 +1335,7 @@ defmodule Pleroma.User do    def external_users(opts \\ []) do      query =        external_users_query() -      |> select([u], struct(u, [:id, :ap_id, :info])) +      |> select([u], struct(u, [:id, :ap_id]))      query =        if opts[:max_id], @@ -1489,7 +1605,7 @@ defmodule Pleroma.User do    end    def showing_reblogs?(%User{} = user, %User{} = target) do -    target.ap_id not in user.muted_reblogs +    not UserRelationship.reblog_mute_exists?(user, target)    end    @doc """ @@ -1731,13 +1847,28 @@ defmodule Pleroma.User do    end    def admin_api_update(user, params) do -    user -    |> cast(params, [ -      :is_moderator, -      :is_admin, -      :show_role -    ]) -    |> update_and_set_cache() +    changeset = +      cast(user, params, [ +        :is_moderator, +        :is_admin, +        :show_role +      ]) + +    with {:ok, updated_user} <- update_and_set_cache(changeset) do +      if user.is_admin && !updated_user.is_admin do +        # Tokens & authorizations containing any admin scopes must be revoked (revoking all). +        # This is an extra safety measure (tokens' admin scopes won't be accepted for non-admins). +        global_sign_out(user) +      end + +      {:ok, updated_user} +    end +  end + +  @doc "Signs user out of all applications" +  def global_sign_out(user) do +    OAuth.Authorization.delete_user_authorizations(user) +    OAuth.Token.delete_user_tokens(user)    end    def mascot_update(user, url) do @@ -1812,23 +1943,6 @@ defmodule Pleroma.User do      |> update_and_set_cache()    end -  defp set_subscribers(user, subscribers) do -    params = %{subscribers: subscribers} - -    user -    |> cast(params, [:subscribers]) -    |> validate_required([:subscribers]) -    |> update_and_set_cache() -  end - -  def add_to_subscribers(user, subscribed) do -    set_subscribers(user, Enum.uniq([subscribed | user.subscribers])) -  end - -  def remove_from_subscribers(user, subscribed) do -    set_subscribers(user, List.delete(user.subscribers, subscribed)) -  end -    defp set_domain_blocks(user, domain_blocks) do      params = %{domain_blocks: domain_blocks} @@ -1846,81 +1960,35 @@ defmodule Pleroma.User do      set_domain_blocks(user, List.delete(user.domain_blocks, domain_blocked))    end -  defp set_blocks(user, blocks) do -    params = %{blocks: blocks} - -    user -    |> cast(params, [:blocks]) -    |> validate_required([:blocks]) -    |> update_and_set_cache() -  end - -  def add_to_block(user, blocked) do -    set_blocks(user, Enum.uniq([blocked | user.blocks])) +  @spec add_to_block(User.t(), User.t()) :: +          {:ok, UserRelationship.t()} | {:error, Ecto.Changeset.t()} +  defp add_to_block(%User{} = user, %User{} = blocked) do +    UserRelationship.create_block(user, blocked)    end -  def remove_from_block(user, blocked) do -    set_blocks(user, List.delete(user.blocks, blocked)) -  end - -  defp set_mutes(user, mutes) do -    params = %{mutes: mutes} - -    user -    |> cast(params, [:mutes]) -    |> validate_required([:mutes]) -    |> update_and_set_cache() +  @spec add_to_block(User.t(), User.t()) :: +          {:ok, UserRelationship.t()} | {:ok, nil} | {:error, Ecto.Changeset.t()} +  defp remove_from_block(%User{} = user, %User{} = blocked) do +    UserRelationship.delete_block(user, blocked)    end -  def add_to_mutes(user, muted, notifications?) do -    with {:ok, user} <- set_mutes(user, Enum.uniq([muted | user.mutes])) do -      set_notification_mutes( -        user, -        Enum.uniq([muted | user.muted_notifications]), -        notifications? -      ) +  defp add_to_mutes(%User{} = user, %User{} = muted_user, notifications?) do +    with {:ok, user_mute} <- UserRelationship.create_mute(user, muted_user), +         {:ok, user_notification_mute} <- +           (notifications? && UserRelationship.create_notification_mute(user, muted_user)) || +             {:ok, nil} do +      {:ok, Enum.filter([user_mute, user_notification_mute], & &1)}      end    end -  def remove_from_mutes(user, muted) do -    with {:ok, user} <- set_mutes(user, List.delete(user.mutes, muted)) do -      set_notification_mutes( -        user, -        List.delete(user.muted_notifications, muted), -        true -      ) +  defp remove_from_mutes(user, %User{} = muted_user) do +    with {:ok, user_mute} <- UserRelationship.delete_mute(user, muted_user), +         {:ok, user_notification_mute} <- +           UserRelationship.delete_notification_mute(user, muted_user) do +      {:ok, [user_mute, user_notification_mute]}      end    end -  defp set_notification_mutes(user, _muted_notifications, false = _notifications?) do -    {:ok, user} -  end - -  defp set_notification_mutes(user, muted_notifications, true = _notifications?) do -    params = %{muted_notifications: muted_notifications} - -    user -    |> cast(params, [:muted_notifications]) -    |> validate_required([:muted_notifications]) -    |> update_and_set_cache() -  end - -  def add_reblog_mute(user, ap_id) do -    params = %{muted_reblogs: user.muted_reblogs ++ [ap_id]} - -    user -    |> cast(params, [:muted_reblogs]) -    |> update_and_set_cache() -  end - -  def remove_reblog_mute(user, ap_id) do -    params = %{muted_reblogs: List.delete(user.muted_reblogs, ap_id)} - -    user -    |> cast(params, [:muted_reblogs]) -    |> update_and_set_cache() -  end -    def set_invisible(user, invisible) do      params = %{invisible: invisible} diff --git a/lib/pleroma/user/notification_setting.ex b/lib/pleroma/user/notification_setting.ex new file mode 100644 index 000000000..f0899613e --- /dev/null +++ b/lib/pleroma/user/notification_setting.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.NotificationSetting do +  use Ecto.Schema +  import Ecto.Changeset + +  @derive Jason.Encoder +  @primary_key false + +  embedded_schema do +    field(:followers, :boolean, default: true) +    field(:follows, :boolean, default: true) +    field(:non_follows, :boolean, default: true) +    field(:non_followers, :boolean, default: true) +    field(:privacy_option, :boolean, default: false) +  end + +  def changeset(schema, params) do +    schema +    |> cast(prepare_attrs(params), [ +      :followers, +      :follows, +      :non_follows, +      :non_followers, +      :privacy_option +    ]) +  end + +  defp prepare_attrs(params) do +    Enum.reduce(params, %{}, fn +      {k, v}, acc when is_binary(v) -> +        Map.put(acc, k, String.downcase(v)) + +      {k, v}, acc -> +        Map.put(acc, k, v) +    end) +  end +end diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index b1bb9d4da..6b55df483 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -103,9 +103,13 @@ defmodule Pleroma.User.Search do      from(q in query, where: q.invisible == false)    end -  defp filter_blocked_user(query, %User{blocks: blocks}) -       when length(blocks) > 0 do -    from(q in query, where: not (q.ap_id in ^blocks)) +  defp filter_blocked_user(query, %User{} = blocker) do +    query +    |> join(:left, [u], b in Pleroma.UserRelationship, +      as: :blocks, +      on: b.relationship_type == ^:block and b.source_id == ^blocker.id and u.id == b.target_id +    ) +    |> where([blocks: b], is_nil(b.target_id))    end    defp filter_blocked_user(query, _), do: query diff --git a/lib/pleroma/user_relationship.ex b/lib/pleroma/user_relationship.ex new file mode 100644 index 000000000..24c724549 --- /dev/null +++ b/lib/pleroma/user_relationship.ex @@ -0,0 +1,92 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.UserRelationship do +  use Ecto.Schema + +  import Ecto.Changeset +  import Ecto.Query + +  alias Pleroma.Repo +  alias Pleroma.User +  alias Pleroma.UserRelationship + +  schema "user_relationships" do +    belongs_to(:source, User, type: FlakeId.Ecto.CompatType) +    belongs_to(:target, User, type: FlakeId.Ecto.CompatType) +    field(:relationship_type, UserRelationshipTypeEnum) + +    timestamps(updated_at: false) +  end + +  for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do +    # Definitions of `create_block/2`, `create_mute/2` etc. +    def unquote(:"create_#{relationship_type}")(source, target), +      do: create(unquote(relationship_type), source, target) + +    # Definitions of `delete_block/2`, `delete_mute/2` etc. +    def unquote(:"delete_#{relationship_type}")(source, target), +      do: delete(unquote(relationship_type), source, target) + +    # Definitions of `block_exists?/2`, `mute_exists?/2` etc. +    def unquote(:"#{relationship_type}_exists?")(source, target), +      do: exists?(unquote(relationship_type), source, target) +  end + +  def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do +    user_relationship +    |> cast(params, [:relationship_type, :source_id, :target_id]) +    |> validate_required([:relationship_type, :source_id, :target_id]) +    |> unique_constraint(:relationship_type, +      name: :user_relationships_source_id_relationship_type_target_id_index +    ) +    |> validate_not_self_relationship() +  end + +  def exists?(relationship_type, %User{} = source, %User{} = target) do +    UserRelationship +    |> where(relationship_type: ^relationship_type, source_id: ^source.id, target_id: ^target.id) +    |> Repo.exists?() +  end + +  def create(relationship_type, %User{} = source, %User{} = target) do +    %UserRelationship{} +    |> changeset(%{ +      relationship_type: relationship_type, +      source_id: source.id, +      target_id: target.id +    }) +    |> Repo.insert( +      on_conflict: :replace_all_except_primary_key, +      conflict_target: [:source_id, :relationship_type, :target_id] +    ) +  end + +  def delete(relationship_type, %User{} = source, %User{} = target) do +    attrs = %{relationship_type: relationship_type, source_id: source.id, target_id: target.id} + +    case Repo.get_by(UserRelationship, attrs) do +      %UserRelationship{} = existing_record -> Repo.delete(existing_record) +      nil -> {:ok, nil} +    end +  end + +  defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do +    changeset +    |> validate_change(:target_id, fn _, target_id -> +      if target_id == get_field(changeset, :source_id) do +        [target_id: "can't be equal to source_id"] +      else +        [] +      end +    end) +    |> validate_change(:source_id, fn _, source_id -> +      if source_id == get_field(changeset, :target_id) do +        [source_id: "can't be equal to target_id"] +      else +        [] +      end +    end) +  end +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 29cc1a17b..c6744e5f2 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -456,17 +456,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      user = User.get_cached_by_ap_id(actor)      to = (object.data["to"] || []) ++ (object.data["cc"] || []) -    with {:ok, object, activity} <- Object.delete(object), +    with create_activity <- Activity.get_create_by_object_ap_id(id),           data <-             %{               "type" => "Delete",               "actor" => actor,               "object" => id,               "to" => to, -             "deleted_activity_id" => activity && activity.id +             "deleted_activity_id" => create_activity && create_activity.id             }             |> maybe_put("id", activity_id),           {:ok, activity} <- insert(data, local, false), +         {:ok, object, _create_activity} <- Object.delete(object),           stream_out_participations(object, user),           _ <- decrease_replies_count_if_reply(object),           {:ok, _actor} <- decrease_note_count_if_public(user, object), @@ -541,6 +542,30 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  def move(%User{} = origin, %User{} = target, local \\ true) do +    params = %{ +      "type" => "Move", +      "actor" => origin.ap_id, +      "object" => origin.ap_id, +      "target" => target.ap_id +    } + +    with true <- origin.ap_id in target.also_known_as, +         {:ok, activity} <- insert(params, local) do +      maybe_federate(activity) + +      BackgroundWorker.enqueue("move_following", %{ +        "origin_id" => origin.id, +        "target_id" => target.id +      }) + +      {:ok, activity} +    else +      false -> {:error, "Target account must have the origin in `alsoKnownAs`"} +      err -> err +    end +  end +    defp fetch_activities_for_context_query(context, opts) do      public = [Pleroma.Constants.as_public()] @@ -724,6 +749,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        |> Map.put("whole_db", true)        |> Map.put("pinned_activity_ids", user.pinned_activities) +    params = +      if User.blocks?(reading_user, user) do +        params +      else +        params +        |> Map.put("blocking_user", reading_user) +        |> Map.put("muting_user", reading_user) +      end +      recipients =        user_activities_recipients(%{          "godmode" => params["godmode"], @@ -895,7 +929,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query    defp restrict_muted(query, %{"muting_user" => %User{} = user} = opts) do -    mutes = user.mutes +    mutes = opts["muted_users_ap_ids"] || User.muted_users_ap_ids(user)      query =        from([activity] in query, @@ -912,8 +946,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_muted(query, _), do: query -  defp restrict_blocked(query, %{"blocking_user" => %User{} = user}) do -    blocks = user.blocks || [] +  defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do +    blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user)      domain_blocks = user.domain_blocks || []      following_ap_ids = @@ -925,14 +959,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      from(        [activity, object: o] in query, -      where: fragment("not (? = ANY(?))", activity.actor, ^blocks), -      where: fragment("not (? && ?)", activity.recipients, ^blocks), +      where: fragment("not (? = ANY(?))", activity.actor, ^blocked_ap_ids), +      where: fragment("not (? && ?)", activity.recipients, ^blocked_ap_ids),        where:          fragment(            "not (?->>'type' = 'Announce' and ?->'to' \\?| ?)",            activity.data,            activity.data, -          ^blocks +          ^blocked_ap_ids          ),        where:          fragment( @@ -973,8 +1007,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_pinned(query, _), do: query -  defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do -    muted_reblogs = user.muted_reblogs || [] +  defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user} = opts) do +    muted_reblogs = opts["reblog_muted_users_ap_ids"] || User.reblog_muted_users_ap_ids(user)      from(        activity in query, @@ -1055,7 +1089,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp maybe_order(query, _), do: query +  defp fetch_activities_query_ap_ids_ops(opts) do +    source_user = opts["muting_user"] +    ap_id_relations = if source_user, do: [:mute, :reblog_mute], else: [] + +    ap_id_relations = +      ap_id_relations ++ +        if opts["blocking_user"] && opts["blocking_user"] == source_user do +          [:block] +        else +          [] +        end + +    preloaded_ap_ids = User.outgoing_relations_ap_ids(source_user, ap_id_relations) + +    restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts) +    restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts) + +    restrict_muted_reblogs_opts = +      Map.merge(%{"reblog_muted_users_ap_ids" => preloaded_ap_ids[:reblog_mute]}, opts) + +    {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} +  end +    def fetch_activities_query(recipients, opts \\ %{}) do +    {restrict_blocked_opts, restrict_muted_opts, restrict_muted_reblogs_opts} = +      fetch_activities_query_ap_ids_ops(opts) +      config = %{        skip_thread_containment: Config.get([:instance, :skip_thread_containment])      } @@ -1075,15 +1135,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> restrict_type(opts)      |> restrict_state(opts)      |> restrict_favorited_by(opts) -    |> restrict_blocked(opts) -    |> restrict_muted(opts) +    |> restrict_blocked(restrict_blocked_opts) +    |> restrict_muted(restrict_muted_opts)      |> restrict_media(opts)      |> restrict_visibility(opts)      |> restrict_thread_visibility(opts, config)      |> restrict_replies(opts)      |> restrict_reblogs(opts)      |> restrict_pinned(opts) -    |> restrict_muted_reblogs(opts) +    |> restrict_muted_reblogs(restrict_muted_reblogs_opts)      |> restrict_instance(opts)      |> Activity.restrict_deactivated_users()      |> exclude_poll_votes(opts) @@ -1175,6 +1235,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      data = Transmogrifier.maybe_fix_user_object(data)      discoverable = data["discoverable"] || false      invisible = data["invisible"] || false +    actor_type = data["type"] || "Person"      user_data = %{        ap_id: data["id"], @@ -1189,7 +1250,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        name: data["name"],        follower_address: data["followers"],        following_address: data["following"], -      bio: data["summary"] +      bio: data["summary"], +      actor_type: actor_type, +      also_known_as: Map.get(data, "alsoKnownAs", [])      }      # nickname can be nil because of virtual actors @@ -1251,13 +1314,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end -  defp collection_private(data) do -    if is_map(data["first"]) and -         data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do +  defp collection_private(%{"first" => first}) do +    if is_map(first) and +         first["type"] in ["CollectionPage", "OrderedCollectionPage"] do        {:ok, false}      else        with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <- -             Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do +             Fetcher.fetch_and_contain_remote_object_from_id(first) do          {:ok, false}        else          {:error, {:ok, %{status: code}}} when code in [401, 403] -> @@ -1272,6 +1335,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  defp collection_private(_data), do: {:ok, true} +    def user_data_from_user_object(data) do      with {:ok, data} <- MRF.filter(data),           {:ok, data} <- object_to_user_data(data) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 15612545b..ecba27bef 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -387,7 +387,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def handle_incoming(%{"id" => nil}, _options), do: :error    def handle_incoming(%{"id" => ""}, _options), do: :error    # length of https:// = 8, should validate better, but good enough for now. -  def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8), +  def handle_incoming(%{"id" => id}, _options) when is_binary(id) and byte_size(id) < 8,      do: :error    # TODO: validate those with a Ecto scheme @@ -669,7 +669,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do        update_data =          new_user_data -        |> Map.take([:avatar, :banner, :bio, :name]) +        |> Map.take([:avatar, :banner, :bio, :name, :also_known_as])          |> Map.put(:fields, fields)          |> Map.put(:locked, locked)          |> Map.put(:invisible, invisible) @@ -857,6 +857,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      end    end +  def handle_incoming( +        %{ +          "type" => "Move", +          "actor" => origin_actor, +          "object" => origin_actor, +          "target" => target_actor +        }, +        _options +      ) do +    with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor), +         {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor), +         true <- origin_actor in target_user.also_known_as do +      ActivityPub.move(origin_user, target_user, false) +    else +      _e -> :error +    end +  end +    def handle_incoming(_, _), do: :error    @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 01aacbde3..2ca805c09 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -722,16 +722,22 @@ defmodule Pleroma.Web.ActivityPub.Utils do          act when is_binary(act) -> act        end -    activity = Activity.get_by_ap_id_with_object(id) -    actor = User.get_by_ap_id(activity.object.data["actor"]) +    case Activity.get_by_ap_id_with_object(id) do +      %Activity{} = activity -> +        %{ +          "type" => "Note", +          "id" => activity.data["id"], +          "content" => activity.object.data["content"], +          "published" => activity.object.data["published"], +          "actor" => +            AccountView.render("show.json", %{ +              user: User.get_by_ap_id(activity.object.data["actor"]) +            }) +        } -    %{ -      "type" => "Note", -      "id" => activity.data["id"], -      "content" => activity.object.data["content"], -      "published" => activity.object.data["published"], -      "actor" => AccountView.render("show.json", %{user: actor}) -    } +      _ -> +        %{"id" => id, "deleted" => true} +    end    end    defp build_flag_object(_), do: [] @@ -788,63 +794,76 @@ defmodule Pleroma.Web.ActivityPub.Utils do      ActivityPub.fetch_activities([], params, :offset)    end -  @spec get_reports_grouped_by_status(%{required(:activity) => String.t()}) :: %{ -          required(:groups) => [ -            %{ -              required(:date) => String.t(), -              required(:account) => %{}, -              required(:status) => %{}, -              required(:actors) => [%User{}], -              required(:reports) => [%Activity{}] -            } -          ], -          required(:total) => integer -        } -  def get_reports_grouped_by_status(groups) do -    parsed_groups = -      groups -      |> Enum.map(fn entry -> -        activity = -          case Jason.decode(entry.activity) do -            {:ok, activity} -> activity -            _ -> build_flag_object(entry.activity) -          end - -        parse_report_group(activity) -      end) - -    %{ -      groups: parsed_groups -    } -  end -    def parse_report_group(activity) do      reports = get_reports_by_status_id(activity["id"])      max_date = Enum.max_by(reports, &NaiveDateTime.from_iso8601!(&1.data["published"]))      actors = Enum.map(reports, & &1.user_actor) +    [%{data: %{"object" => [account_id | _]}} | _] = reports + +    account = +      AccountView.render("show.json", %{ +        user: User.get_by_ap_id(account_id) +      }) + +    status = get_status_data(activity)      %{        date: max_date.data["published"], -      account: activity["actor"], -      status: %{ -        id: activity["id"], -        content: activity["content"], -        published: activity["published"] -      }, +      account: account, +      status: status,        actors: Enum.uniq(actors),        reports: reports      }    end +  defp get_status_data(status) do +    case status["deleted"] do +      true -> +        %{ +          "id" => status["id"], +          "deleted" => true +        } + +      _ -> +        Activity.get_by_ap_id(status["id"]) +    end +  end +    def get_reports_by_status_id(ap_id) do      from(a in Activity,        where: fragment("(?)->>'type' = 'Flag'", a.data), -      where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]) +      where: fragment("(?)->'object' @> ?", a.data, ^[%{id: ap_id}]), +      or_where: fragment("(?)->'object' @> ?", a.data, ^[ap_id])      )      |> Activity.with_preloaded_user_actor()      |> Repo.all()    end +  @spec get_reports_grouped_by_status([String.t()]) :: %{ +          required(:groups) => [ +            %{ +              required(:date) => String.t(), +              required(:account) => %{}, +              required(:status) => %{}, +              required(:actors) => [%User{}], +              required(:reports) => [%Activity{}] +            } +          ] +        } +  def get_reports_grouped_by_status(activity_ids) do +    parsed_groups = +      activity_ids +      |> Enum.map(fn id -> +        id +        |> build_flag_object() +        |> parse_report_group() +      end) + +    %{ +      groups: parsed_groups +    } +  end +    @spec get_reported_activities() :: [            %{              required(:activity) => String.t(), @@ -852,17 +871,23 @@ defmodule Pleroma.Web.ActivityPub.Utils do            }          ]    def get_reported_activities do -    from(a in Activity, -      where: fragment("(?)->>'type' = 'Flag'", a.data), +    reported_activities_query = +      from(a in Activity, +        where: fragment("(?)->>'type' = 'Flag'", a.data), +        select: %{ +          activity: fragment("jsonb_array_elements((? #- '{object,0}')->'object')", a.data) +        }, +        group_by: fragment("activity") +      ) + +    from(a in subquery(reported_activities_query), +      distinct: true,        select: %{ -        date: fragment("max(?->>'published') date", a.data), -        activity: -          fragment("jsonb_array_elements_text((? #- '{object,0}')->'object') activity", a.data) -      }, -      group_by: fragment("activity"), -      order_by: fragment("date DESC") +        id: fragment("COALESCE(?->>'id'::text, ? #>> '{}')", a.activity, a.activity) +      }      )      |> Repo.all() +    |> Enum.map(& &1.id)    end    def update_report_state(%Activity{} = activity, state) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index cf08045c9..9059aa634 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -91,7 +91,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do      %{        "id" => user.ap_id, -      "type" => "Person", +      "type" => user.actor_type,        "following" => "#{user.ap_id}/following",        "followers" => "#{user.ap_id}/followers",        "inbox" => "#{user.ap_id}/inbox", diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index cd4097493..e172f6d3f 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Visibility do    @spec is_public?(Object.t() | Activity.t() | map()) :: boolean()    def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false    def is_public?(%Object{data: data}), do: is_public?(data) +  def is_public?(%Activity{data: %{"type" => "Move"}}), do: true    def is_public?(%Activity{data: data}), do: is_public?(data)    def is_public?(%{"directMessage" => true}), do: false    def is_public?(data), do: Utils.label_in_message?(Pleroma.Constants.as_public(), data) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 24fdc3c82..0a8a56cd8 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -30,13 +30,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    plug(      OAuthScopesPlug, -    %{scopes: ["read:accounts"]} +    %{scopes: ["read:accounts"], admin: true}      when action in [:list_users, :user_show, :right_get, :invites]    )    plug(      OAuthScopesPlug, -    %{scopes: ["write:accounts"]} +    %{scopes: ["write:accounts"], admin: true}      when action in [             :get_invite_token,             :revoke_invite, @@ -58,35 +58,37 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    plug(      OAuthScopesPlug, -    %{scopes: ["read:reports"]} when action in [:list_reports, :report_show] +    %{scopes: ["read:reports"], admin: true} +    when action in [:list_reports, :report_show]    )    plug(      OAuthScopesPlug, -    %{scopes: ["write:reports"]} +    %{scopes: ["write:reports"], admin: true}      when action in [:report_update_state, :report_respond]    )    plug(      OAuthScopesPlug, -    %{scopes: ["read:statuses"]} when action == :list_user_statuses +    %{scopes: ["read:statuses"], admin: true} +    when action == :list_user_statuses    )    plug(      OAuthScopesPlug, -    %{scopes: ["write:statuses"]} +    %{scopes: ["write:statuses"], admin: true}      when action in [:status_update, :status_delete]    )    plug(      OAuthScopesPlug, -    %{scopes: ["read"]} +    %{scopes: ["read"], admin: true}      when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]    )    plug(      OAuthScopesPlug, -    %{scopes: ["write"]} +    %{scopes: ["write"], admin: true}      when action in [:relay_follow, :relay_unfollow, :config_update]    ) @@ -647,11 +649,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do    end    def list_grouped_reports(conn, _params) do -    reports = Utils.get_reported_activities() +    statuses = Utils.get_reported_activities()      conn      |> put_view(ReportView) -    |> render("index_grouped.json", Utils.get_reports_grouped_by_status(reports)) +    |> render("index_grouped.json", Utils.get_reports_grouped_by_status(statuses))    end    def report_show(conn, %{"id" => id}) do diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index ca88595c7..13602efd9 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -4,6 +4,7 @@  defmodule Pleroma.Web.AdminAPI.ReportView do    use Pleroma.Web, :view +  alias Pleroma.Activity    alias Pleroma.HTML    alias Pleroma.User    alias Pleroma.Web.AdminAPI.Report @@ -45,10 +46,16 @@ defmodule Pleroma.Web.AdminAPI.ReportView do    def render("index_grouped.json", %{groups: groups}) do      reports =        Enum.map(groups, fn group -> +        status = +          case group.status do +            %Activity{} = activity -> StatusView.render("show.json", %{activity: activity}) +            _ -> group.status +          end +          %{            date: group[:date],            account: group[:account], -          status: group[:status], +          status: Map.put_new(status, "deleted", false),            actors: Enum.map(group[:actors], &merge_account_views/1),            reports:              group[:reports] diff --git a/lib/pleroma/web/chat_channel.ex b/lib/pleroma/web/chat_channel.ex index 08841a3e8..840414933 100644 --- a/lib/pleroma/web/chat_channel.ex +++ b/lib/pleroma/web/chat_channel.ex @@ -20,7 +20,7 @@ defmodule Pleroma.Web.ChatChannel do    def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}} = socket) do      text = String.trim(text) -    if String.length(text) > 0 do +    if String.length(text) in 1..Pleroma.Config.get([:instance, :chat_limit]) do        author = User.get_cached_by_nickname(user_name)        author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author)        message = ChatChannelState.add_message(%{text: text, author: author}) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index fe6e26a90..2f3bcfc3c 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.CommonAPI do    alias Pleroma.Object    alias Pleroma.ThreadMute    alias Pleroma.User +  alias Pleroma.UserRelationship    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.ActivityPub.Visibility @@ -32,7 +33,7 @@ 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), -         {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do +         {:ok, _subscription} <- User.unsubscribe(follower, unfollowed) do        {:ok, follower}      end    end @@ -420,15 +421,11 @@ defmodule Pleroma.Web.CommonAPI do    defp set_visibility(activity, _), do: {:ok, activity} -  def hide_reblogs(user, %{ap_id: ap_id} = _muted) do -    if ap_id not in user.muted_reblogs do -      User.add_reblog_mute(user, ap_id) -    end +  def hide_reblogs(%User{} = user, %User{} = target) do +    UserRelationship.create_reblog_mute(user, target)    end -  def show_reblogs(user, %{ap_id: ap_id} = _muted) do -    if ap_id in user.muted_reblogs do -      User.remove_reblog_mute(user, ap_id) -    end +  def show_reblogs(%User{} = user, %User{} = target) do +    UserRelationship.delete_reblog_mute(user, target)    end  end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 88a5f434a..a9b164d9a 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -451,6 +451,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do      recipients ++ to    end +  def maybe_notify_to_recipients(recipients, _), do: recipients +    def maybe_notify_mentioned_recipients(          recipients,          %Activity{data: %{"to" => _to, "type" => type} = data} = activity @@ -492,7 +494,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do      with %User{} = user <- User.get_cached_by_ap_id(actor) do        subscriber_ids =          user -        |> User.subscribers() +        |> User.subscriber_users()          |> Enum.filter(&Visibility.visible_for_user?(activity, &1))          |> Enum.map(& &1.ap_id) @@ -502,6 +504,17 @@ defmodule Pleroma.Web.CommonAPI.Utils do    def maybe_notify_subscribers(recipients, _), do: recipients +  def maybe_notify_followers(recipients, %Activity{data: %{"type" => "Move"}} = activity) do +    with %User{} = user <- User.get_cached_by_ap_id(activity.actor) do +      user +      |> User.get_followers() +      |> Enum.map(& &1.ap_id) +      |> Enum.concat(recipients) +    end +  end + +  def maybe_notify_followers(recipients, _), do: recipients +    def maybe_extract_mentions(%{"tag" => tag}) do      tag      |> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 49735b5c2..bbea31682 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -61,14 +61,7 @@ defmodule Pleroma.Web.Endpoint do    plug(Plug.RequestId)    plug(Plug.Logger) -  plug( -    Plug.Parsers, -    parsers: [:urlencoded, :multipart, :json], -    pass: ["*/*"], -    json_decoder: Jason, -    length: Pleroma.Config.get([:instance, :upload_limit]), -    body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} -  ) +  plug(Pleroma.Plugs.Parsers)    plug(Plug.MethodOverride)    plug(Plug.Head) diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 5f2544640..38d14256f 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -152,6 +152,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do          :hide_favorites,          :show_role,          :skip_thread_containment, +        :allow_following_move,          :discoverable        ]        |> Enum.reduce(%{}, fn key, acc -> @@ -187,6 +188,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do          {:ok, Map.merge(user.pleroma_settings_store, value)}        end)        |> add_if_present(params, "default_scope", :default_scope) +      |> add_if_present(params, "actor_type", :actor_type)      emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") @@ -248,7 +250,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    @doc "GET /api/v1/accounts/:id/statuses"    def statuses(%{assigns: %{user: reading_user}} = conn, params) do      with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do -      params = Map.put(params, "tag", params["tagged"]) +      params = +        params +        |> Map.put("tag", params["tagged"]) +        |> Map.delete("godmode") +        activities = ActivityPub.fetch_user_activities(user, reading_user, params)        conn @@ -323,7 +329,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do      notifications? = params |> Map.get("notifications", true) |> truthy_param?() -    with {:ok, muter} <- User.mute(muter, muted, notifications?) do +    with {:ok, _user_relationships} <- User.mute(muter, muted, notifications?) do        render(conn, "relationship.json", user: muter, target: muted)      else        {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -332,7 +338,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    @doc "POST /api/v1/accounts/:id/unmute"    def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do -    with {:ok, muter} <- User.unmute(muter, muted) do +    with {:ok, _user_relationships} <- User.unmute(muter, muted) do        render(conn, "relationship.json", user: muter, target: muted)      else        {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -341,7 +347,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    @doc "POST /api/v1/accounts/:id/block"    def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do -    with {:ok, blocker} <- User.block(blocker, blocked), +    with {:ok, _user_block} <- User.block(blocker, blocked),           {:ok, _activity} <- ActivityPub.block(blocker, blocked) do        render(conn, "relationship.json", user: blocker, target: blocked)      else @@ -351,7 +357,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    @doc "POST /api/v1/accounts/:id/unblock"    def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do -    with {:ok, blocker} <- User.unblock(blocker, blocked), +    with {:ok, _user_block} <- User.unblock(blocker, blocked),           {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do        render(conn, "relationship.json", user: blocker, target: blocked)      else @@ -373,12 +379,14 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do    @doc "GET /api/v1/mutes"    def mutes(%{assigns: %{user: user}} = conn, _) do -    render(conn, "index.json", users: User.muted_users(user), for: user, as: :user) +    users = User.muted_users(user, _restrict_deactivated = true) +    render(conn, "index.json", users: users, for: user, as: :user)    end    @doc "GET /api/v1/blocks"    def blocks(%{assigns: %{user: user}} = conn, _) do -    render(conn, "index.json", users: User.blocked_users(user), for: user, as: :user) +    users = User.blocked_users(user, _restrict_deactivated = true) +    render(conn, "index.json", users: users, for: user, as: :user)    end    @doc "GET /api/v1/endorsements" diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index d875a5788..b1816370e 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -24,19 +24,16 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do      with {:ok, follower, _followed, _} <- result do        options = cast_params(params) - -      case reblogs_visibility(options[:reblogs], result) do -        {:ok, follower} -> {:ok, follower} -        _ -> {:ok, follower} -      end +      set_reblogs_visibility(options[:reblogs], result) +      {:ok, follower}      end    end -  defp reblogs_visibility(false, {:ok, follower, followed, _}) do +  defp set_reblogs_visibility(false, {:ok, follower, followed, _}) do      CommonAPI.hide_reblogs(follower, followed)    end -  defp reblogs_visibility(_, {:ok, follower, followed, _}) do +  defp set_reblogs_visibility(_, {:ok, follower, followed, _}) do      CommonAPI.show_reblogs(follower, followed)    end @@ -73,7 +70,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do        exclude_types: {:array, :string},        exclude_visibilities: {:array, :string},        reblogs: :boolean, -      with_muted: :boolean +      with_muted: :boolean, +      with_move: :boolean      }      changeset = cast({%{}, param_types}, params, Map.keys(param_types)) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 1068f8823..a5420f480 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -50,8 +50,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do        id: to_string(target.id),        following: User.following?(user, target),        followed_by: User.following?(target, user), -      blocking: User.blocks_ap_id?(user, target), -      blocked_by: User.blocks_ap_id?(target, user), +      blocking: User.blocks_user?(user, target), +      blocked_by: User.blocks_user?(target, user),        muting: User.mutes?(user, target),        muting_notifications: User.muted_notifications?(user, target),        subscribing: User.subscribed_to?(user, target), @@ -86,7 +86,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          0        end -    bot = (user.source_data["type"] || "Person") in ["Application", "Service"] +    bot = user.actor_type in ["Application", "Service"]      emojis =        (user.source_data["tag"] || []) @@ -137,7 +137,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do          sensitive: false,          fields: user.raw_fields,          pleroma: %{ -          discoverable: user.discoverable +          discoverable: user.discoverable, +          actor_type: user.actor_type          }        }, @@ -162,6 +163,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      |> maybe_put_chat_token(user, opts[:for], opts)      |> maybe_put_activation_status(user, opts[:for])      |> maybe_put_follow_requests_count(user, opts[:for]) +    |> maybe_put_allow_following_move(user, opts[:for])      |> maybe_put_unread_conversation_count(user, opts[:for])    end @@ -238,6 +240,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    defp maybe_put_notification_settings(data, _, _), do: data +  defp maybe_put_allow_following_move(data, %User{id: user_id} = user, %User{id: user_id}) do +    Kernel.put_in(data, [:pleroma, :allow_following_move], user.allow_following_move) +  end + +  defp maybe_put_allow_following_move(data, _, _), do: data +    defp maybe_put_activation_status(data, user, %User{is_admin: true}) do      Kernel.put_in(data, [:pleroma, :deactivated], user.deactivated)    end diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index 5e3dbe728..ddd7f5318 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -37,32 +37,24 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do        }        case mastodon_type do -        "mention" -> -          response -          |> Map.merge(%{ -            status: StatusView.render("show.json", %{activity: activity, for: user}) -          }) - -        "favourite" -> -          response -          |> Map.merge(%{ -            status: StatusView.render("show.json", %{activity: parent_activity, for: user}) -          }) - -        "reblog" -> -          response -          |> Map.merge(%{ -            status: StatusView.render("show.json", %{activity: parent_activity, for: user}) -          }) - -        "follow" -> -          response - -        _ -> -          nil +        "mention" -> put_status(response, activity, user) +        "favourite" -> put_status(response, parent_activity, user) +        "reblog" -> put_status(response, parent_activity, user) +        "move" -> put_target(response, activity, user) +        "follow" -> response +        _ -> nil        end      else        _ -> nil      end    end + +  defp put_status(response, activity, user) do +    Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user})) +  end + +  defp put_target(response, activity, user) do +    target = User.get_cached_by_ap_id(activity.data["target"]) +    Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user})) +  end  end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 2aee8cab2..87acdec97 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -222,7 +222,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do           {:user_active, true} <- {:user_active, !user.deactivated},           {:password_reset_pending, false} <-             {:password_reset_pending, user.password_reset_pending}, -         {:ok, scopes} <- validate_scopes(app, params), +         {:ok, scopes} <- validate_scopes(app, params, user),           {:ok, auth} <- Authorization.create_authorization(app, user, scopes),           {:ok, token} <- Token.exchange_token(app, auth) do        json(conn, Token.Response.build(user, token)) @@ -471,7 +471,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do             {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},           %App{} = app <- Repo.get_by(App, client_id: client_id),           true <- redirect_uri in String.split(app.redirect_uris), -         {:ok, scopes} <- validate_scopes(app, auth_attrs), +         {:ok, scopes} <- validate_scopes(app, auth_attrs, user),           {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do        Authorization.create_authorization(app, user, scopes)      end @@ -487,12 +487,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do    defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),      do: put_session(conn, :registration_id, registration_id) -  @spec validate_scopes(App.t(), map()) :: +  @spec validate_scopes(App.t(), map(), User.t()) ::            {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} -  defp validate_scopes(app, params) do +  defp validate_scopes(%App{} = app, params, %User{} = user) do      params      |> Scopes.fetch_scopes(app.scopes) -    |> Scopes.validate(app.scopes) +    |> Scopes.validate(app.scopes, user)    end    def default_redirect_uri(%App{} = app) do diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex index 48bd14407..5e04652c2 100644 --- a/lib/pleroma/web/oauth/scopes.ex +++ b/lib/pleroma/web/oauth/scopes.ex @@ -7,6 +7,9 @@ defmodule Pleroma.Web.OAuth.Scopes do    Functions for dealing with scopes.    """ +  alias Pleroma.Plugs.OAuthScopesPlug +  alias Pleroma.User +    @doc """    Fetch scopes from request params. @@ -53,15 +56,36 @@ defmodule Pleroma.Web.OAuth.Scopes do    @doc """    Validates scopes.    """ -  @spec validate(list() | nil, list()) :: +  @spec validate(list() | nil, list(), User.t()) ::            {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} -  def validate([], _app_scopes), do: {:error, :missing_scopes} -  def validate(nil, _app_scopes), do: {:error, :missing_scopes} +  def validate(blank_scopes, _app_scopes, _user) when blank_scopes in [nil, []], +    do: {:error, :missing_scopes} -  def validate(scopes, app_scopes) do -    case Pleroma.Plugs.OAuthScopesPlug.filter_descendants(scopes, app_scopes) do +  def validate(scopes, app_scopes, %User{} = user) do +    with {:ok, _} <- ensure_scopes_support(scopes, app_scopes), +         {:ok, scopes} <- authorize_admin_scopes(scopes, app_scopes, user) do +      {:ok, scopes} +    end +  end + +  defp ensure_scopes_support(scopes, app_scopes) do +    case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do        ^scopes -> {:ok, scopes}        _ -> {:error, :unsupported_scopes}      end    end + +  defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do +    if user.is_admin || !contains_admin_scopes?(scopes) || !contains_admin_scopes?(app_scopes) do +      {:ok, scopes} +    else +      {:error, :unsupported_scopes} +    end +  end + +  def contains_admin_scopes?(scopes) do +    scopes +    |> OAuthScopesPlug.filter_descendants(["admin"]) +    |> Enum.any?() +  end  end diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex index f639f9c6f..3c9c580d5 100644 --- a/lib/pleroma/web/oauth/token/clean_worker.ex +++ b/lib/pleroma/web/oauth/token/clean_worker.ex @@ -11,11 +11,6 @@ defmodule Pleroma.Web.OAuth.Token.CleanWorker do    @ten_seconds 10_000    @one_day 86_400_000 -  @interval Pleroma.Config.get( -              [:oauth2, :clean_expired_tokens_interval], -              @one_day -            ) -    alias Pleroma.Web.OAuth.Token    alias Pleroma.Workers.BackgroundWorker @@ -29,8 +24,9 @@ defmodule Pleroma.Web.OAuth.Token.CleanWorker do    @doc false    def handle_info(:perform, state) do      BackgroundWorker.enqueue("clean_expired_tokens", %{}) +    interval = Pleroma.Config.get([:oauth2, :clean_expired_tokens_interval], @one_day) -    Process.send_after(self(), :perform, @interval) +    Process.send_after(self(), :perform, interval)      {:noreply, state}    end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index bc2f1017c..773cd9a97 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -144,7 +144,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do    @doc "POST /api/v1/pleroma/accounts/:id/subscribe"    def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do -    with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do +    with {:ok, _subscription} <- User.subscribe(user, subscription_target) do        render(conn, "relationship.json", user: user, target: subscription_target)      else        {:error, message} -> json_response(conn, :forbidden, %{error: message}) @@ -153,7 +153,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do    @doc "POST /api/v1/pleroma/accounts/:id/unsubscribe"    def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do -    with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do +    with {:ok, _subscription} <- User.unsubscribe(user, subscription_target) do        render(conn, "relationship.json", user: user, target: subscription_target)      else        {:error, message} -> json_response(conn, :forbidden, %{error: message}) diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index a474d41d4..69dfa92e3 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do    plug(      OAuthScopesPlug, -    %{scopes: ["write"]} +    %{scopes: ["write"], admin: true}      when action in [             :create,             :delete, diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 3de7af708..34ec1d8d9 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -16,14 +16,14 @@ defmodule Pleroma.Web.Push.Impl do    require Logger    import Ecto.Query -  @types ["Create", "Follow", "Announce", "Like"] +  @types ["Create", "Follow", "Announce", "Like", "Move"]    @doc "Performs sending notifications for user subscriptions"    @spec perform(Notification.t()) :: list(any) | :error    def perform(          %{ -          activity: %{data: %{"type" => activity_type}, id: activity_id} = activity, -          user_id: user_id +          activity: %{data: %{"type" => activity_type}} = activity, +          user: %User{id: user_id}          } = notif        )        when activity_type in @types do @@ -39,18 +39,17 @@ defmodule Pleroma.Web.Push.Impl do      for subscription <- fetch_subsriptions(user_id),          get_in(subscription.data, ["alerts", type]) do        %{ -        title: format_title(notif),          access_token: subscription.token.token, -        body: format_body(notif, actor, object),          notification_id: notif.id,          notification_type: type,          icon: avatar_url,          preferred_locale: "en",          pleroma: %{ -          activity_id: activity_id, +          activity_id: notif.activity.id,            direct_conversation_id: direct_conversation_id          }        } +      |> Map.merge(build_content(notif, actor, object))        |> Jason.encode!()        |> push_message(build_sub(subscription), gcm_api_key, subscription)      end @@ -100,6 +99,24 @@ defmodule Pleroma.Web.Push.Impl do      }    end +  def build_content( +        %{ +          activity: %{data: %{"directMessage" => true}}, +          user: %{notification_settings: %{privacy_option: true}} +        }, +        actor, +        _ +      ) do +    %{title: "New Direct Message", body: "@#{actor.nickname}"} +  end + +  def build_content(notif, actor, object) do +    %{ +      title: format_title(notif), +      body: format_body(notif, actor, object) +    } +  end +    def format_body(          %{activity: %{data: %{"type" => "Create"}}},          actor, diff --git a/lib/pleroma/web/streamer/worker.ex b/lib/pleroma/web/streamer/worker.ex index 33b24840d..a1b445f2f 100644 --- a/lib/pleroma/web/streamer/worker.ex +++ b/lib/pleroma/web/streamer/worker.ex @@ -129,16 +129,17 @@ defmodule Pleroma.Web.Streamer.Worker do    end    defp should_send?(%User{} = user, %Activity{} = item) do -    blocks = user.blocks || [] -    mutes = user.mutes || [] -    reblog_mutes = user.muted_reblogs || [] -    recipient_blocks = MapSet.new(blocks ++ mutes) +    %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = +      User.outgoing_relations_ap_ids(user, [:block, :mute, :reblog_mute]) + +    recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)      recipients = MapSet.new(item.recipients)      domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.domain_blocks)      with parent <- Object.normalize(item) || item, -         true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), -         true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), +         true <- +           Enum.all?([blocked_ap_ids, muted_ap_ids, reblog_muted_ap_ids], &(item.actor not in &1)), +         true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)),           true <- MapSet.disjoint?(recipients, recipient_blocks),           %{host: item_host} <- URI.parse(item.actor),           %{host: parent_host} <- URI.parse(parent.data["actor"]), diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex index 7ffc8eabe..323a4da1e 100644 --- a/lib/pleroma/workers/background_worker.ex +++ b/lib/pleroma/workers/background_worker.ex @@ -71,4 +71,11 @@ defmodule Pleroma.Workers.BackgroundWorker do      activity = Activity.get_by_id(activity_id)      Pleroma.Web.RichMedia.Helpers.perform(:fetch, activity)    end + +  def perform(%{"op" => "move_following", "origin_id" => origin_id, "target_id" => target_id}, _) do +    origin = User.get_cached_by_id(origin_id) +    target = User.get_cached_by_id(target_id) + +    Pleroma.FollowingRelationship.move_following(origin, target) +  end  end diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex index 61b451e3e..a978c4013 100644 --- a/lib/pleroma/workers/web_pusher_worker.ex +++ b/lib/pleroma/workers/web_pusher_worker.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Workers.WebPusherWorker do      notification =        Notification        |> Repo.get(notification_id) -      |> Repo.preload([:activity]) +      |> Repo.preload([:activity, :user])      Pleroma.Web.Push.Impl.perform(notification)    end  | 
