diff options
| author | Maksim Pechnikov <parallel588@gmail.com> | 2019-06-04 09:49:08 +0300 | 
|---|---|---|
| committer | Maksim Pechnikov <parallel588@gmail.com> | 2019-06-04 09:49:08 +0300 | 
| commit | 4f2e359687b8866a26130314315415a787678881 (patch) | |
| tree | 51e29be10335dab73962952c2f1b0407cbd52ecb /lib | |
| parent | 64ada7f960eb45d5e06d431c0c27be1014106ff9 (diff) | |
| parent | 0f8f63c6bb4a26615277f43ed4f30f032d71df83 (diff) | |
| download | pleroma-4f2e359687b8866a26130314315415a787678881.tar.gz pleroma-4f2e359687b8866a26130314315415a787678881.zip  | |
Merge branch 'develop' into issue/941
Diffstat (limited to 'lib')
23 files changed, 618 insertions, 103 deletions
diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index 238c1acf2..bc97b39ca 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -49,7 +49,7 @@ defmodule Pleroma.Conversation do      with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity),           "Create" <- activity.data["type"],           object <- Pleroma.Object.normalize(activity), -         "Note" <- object.data["type"], +         true <- object.data["type"] in ["Note", "Question"],           ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do        {:ok, conversation} = create_for_ap_id(ap_id) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 844264307..46f2107b1 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -127,10 +127,15 @@ defmodule Pleroma.Notification do    def create_notifications(%Activity{data: %{"to" => _, "type" => type}} = activity)        when type in ["Create", "Like", "Announce", "Follow"] do -    users = get_notified_from_activity(activity) - -    notifications = Enum.map(users, fn user -> create_notification(activity, user) end) -    {:ok, notifications} +    object = Object.normalize(activity) + +    unless object && object.data["type"] == "Answer" do +      users = get_notified_from_activity(activity) +      notifications = Enum.map(users, fn user -> create_notification(activity, user) end) +      {:ok, notifications} +    else +      {:ok, []} +    end    end    def create_notifications(_), do: {:ok, []} @@ -166,7 +171,16 @@ defmodule Pleroma.Notification do    def get_notified_from_activity(_, _local_only), do: []    def skip?(activity, user) do -    [:self, :blocked, :local, :muted, :followers, :follows, :recently_followed] +    [ +      :self, +      :blocked, +      :muted, +      :followers, +      :follows, +      :non_followers, +      :non_follows, +      :recently_followed +    ]      |> Enum.any?(&skip?(&1, activity, user))    end @@ -179,12 +193,6 @@ defmodule Pleroma.Notification do      User.blocks?(user, %{ap_id: actor})    end -  def skip?(:local, %{local: true}, %{info: %{notification_settings: %{"local" => false}}}), -    do: true - -  def skip?(:local, %{local: false}, %{info: %{notification_settings: %{"remote" => false}}}), -    do: true -    def skip?(:muted, activity, user) do      actor = activity.data["actor"] @@ -201,12 +209,32 @@ defmodule Pleroma.Notification do      User.following?(follower, user)    end +  def skip?( +        :non_followers, +        activity, +        %{info: %{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, %{info: %{notification_settings: %{"follows" => false}}} = user) do      actor = activity.data["actor"]      followed = User.get_cached_by_ap_id(actor)      User.following?(user, followed)    end +  def skip?( +        :non_follows, +        activity, +        %{info: %{notification_settings: %{"non_follows" => false}}} = user +      ) do +    actor = activity.data["actor"] +    followed = User.get_cached_by_ap_id(actor) +    !User.following?(user, followed) +  end +    def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do      actor = activity.data["actor"] diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index cc6fc9c5d..4b181ec59 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -35,6 +35,9 @@ defmodule Pleroma.Object do      |> unique_constraint(:ap_id, name: :objects_unique_apid_index)    end +  def get_by_id(nil), do: nil +  def get_by_id(id), do: Repo.get(Object, id) +    def get_by_ap_id(nil), do: nil    def get_by_ap_id(ap_id) do @@ -195,4 +198,34 @@ defmodule Pleroma.Object do        _ -> {:error, "Not found"}      end    end + +  def increase_vote_count(ap_id, name) do +    with %Object{} = object <- Object.normalize(ap_id), +         "Question" <- object.data["type"] do +      multiple = Map.has_key?(object.data, "anyOf") + +      options = +        (object.data["anyOf"] || object.data["oneOf"] || []) +        |> Enum.map(fn +          %{"name" => ^name} = option -> +            Kernel.update_in(option["replies"]["totalItems"], &(&1 + 1)) + +          option -> +            option +        end) + +      data = +        if multiple do +          Map.put(object.data, "anyOf", options) +        else +          Map.put(object.data, "oneOf", options) +        end + +      object +      |> Object.change(%{data: data}) +      |> update_and_set_cache() +    else +      _ -> :noop +    end +  end  end diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex index 983e156f5..285d57309 100644 --- a/lib/pleroma/reverse_proxy.ex +++ b/lib/pleroma/reverse_proxy.ex @@ -61,8 +61,6 @@ defmodule Pleroma.ReverseProxy do    * `http`: options for [hackney](https://github.com/benoitc/hackney).    """ -  @hackney Pleroma.Config.get(:hackney, :hackney) -    @default_hackney_options []    @inline_content_types [ @@ -148,7 +146,7 @@ defmodule Pleroma.ReverseProxy do      Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")      method = method |> String.downcase() |> String.to_existing_atom() -    case @hackney.request(method, url, headers, "", hackney_opts) do +    case :hackney.request(method, url, headers, "", hackney_opts) do        {:ok, code, headers, client} when code in @valid_resp_codes ->          {:ok, code, downcase_headers(headers), client} @@ -198,7 +196,7 @@ defmodule Pleroma.ReverseProxy do               duration,               Keyword.get(opts, :max_read_duration, @max_read_duration)             ), -         {:ok, data} <- @hackney.stream_body(client), +         {:ok, data} <- :hackney.stream_body(client),           {:ok, duration} <- increase_read_duration(duration),           sent_so_far = sent_so_far + byte_size(data),           :ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)), diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index d1fb4fe75..08e43ff0f 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -44,9 +44,15 @@ defmodule Pleroma.User.Info do      field(:pinned_activities, {:array, :string}, default: [])      field(:mascot, :map, default: nil)      field(:emoji, {:array, :map}, default: []) +    field(:pleroma_settings_store, :map, default: %{})      field(:notification_settings, :map, -      default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true} +      default: %{ +        "followers" => true, +        "follows" => true, +        "non_follows" => true, +        "non_followers" => true +      }      )      field(:skip_thread_containment, :boolean, default: false) @@ -69,10 +75,15 @@ defmodule Pleroma.User.Info do    end    def update_notification_settings(info, settings) do +    settings = +      settings +      |> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end) +      |> Map.new() +      notification_settings =        info.notification_settings        |> Map.merge(settings) -      |> Map.take(["remote", "local", "followers", "follows"]) +      |> Map.take(["followers", "follows", "non_follows", "non_followers"])      params = %{notification_settings: notification_settings} @@ -211,7 +222,8 @@ defmodule Pleroma.User.Info do        :hide_favorites,        :background,        :show_role, -      :skip_thread_containment +      :skip_thread_containment, +      :pleroma_settings_store      ])    end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index f121ef01b..8c4d0c15d 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -109,6 +109,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def decrease_replies_count_if_reply(_object), do: :noop +  def increase_poll_votes_if_vote(%{ +        "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, +        "type" => "Create" +      }) do +    Object.increase_vote_count(reply_ap_id, name) +  end + +  def increase_poll_votes_if_vote(_create_data), do: :noop +    def insert(map, local \\ true, fake \\ false) when is_map(map) do      with nil <- Activity.normalize(map),           map <- lazy_put_activity_defaults(map, fake), @@ -184,40 +193,42 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      public = "https://www.w3.org/ns/activitystreams#Public"      if activity.data["type"] in ["Create", "Announce", "Delete"] do -      Pleroma.Web.Streamer.stream("user", activity) -      Pleroma.Web.Streamer.stream("list", activity) +      object = Object.normalize(activity) +      # Do not stream out poll replies +      unless object.data["type"] == "Answer" do +        Pleroma.Web.Streamer.stream("user", activity) +        Pleroma.Web.Streamer.stream("list", activity) -      if Enum.member?(activity.data["to"], public) do -        Pleroma.Web.Streamer.stream("public", activity) +        if Enum.member?(activity.data["to"], public) do +          Pleroma.Web.Streamer.stream("public", activity) -        if activity.local do -          Pleroma.Web.Streamer.stream("public:local", activity) -        end - -        if activity.data["type"] in ["Create"] do -          object = Object.normalize(activity) +          if activity.local do +            Pleroma.Web.Streamer.stream("public:local", activity) +          end -          object.data -          |> Map.get("tag", []) -          |> Enum.filter(fn tag -> is_bitstring(tag) end) -          |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) +          if activity.data["type"] in ["Create"] do +            object.data +            |> Map.get("tag", []) +            |> Enum.filter(fn tag -> is_bitstring(tag) end) +            |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) -          if object.data["attachment"] != [] do -            Pleroma.Web.Streamer.stream("public:media", activity) +            if object.data["attachment"] != [] do +              Pleroma.Web.Streamer.stream("public:media", activity) -            if activity.local do -              Pleroma.Web.Streamer.stream("public:local:media", activity) +              if activity.local do +                Pleroma.Web.Streamer.stream("public:local:media", activity) +              end              end            end +        else +          # TODO: Write test, replace with visibility test +          if !Enum.member?(activity.data["cc"] || [], public) && +               !Enum.member?( +                 activity.data["to"], +                 User.get_cached_by_ap_id(activity.data["actor"]).follower_address +               ), +             do: Pleroma.Web.Streamer.stream("direct", activity)          end -      else -        # TODO: Write test, replace with visibility test -        if !Enum.member?(activity.data["cc"] || [], public) && -             !Enum.member?( -               activity.data["to"], -               User.get_cached_by_ap_id(activity.data["actor"]).follower_address -             ), -           do: Pleroma.Web.Streamer.stream("direct", activity)        end      end    end @@ -236,6 +247,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do           {:ok, activity} <- insert(create_data, local, fake),           {:fake, false, activity} <- {:fake, fake, activity},           _ <- increase_replies_count_if_reply(create_data), +         _ <- increase_poll_votes_if_vote(create_data),           # Changing note count prior to enqueuing federation task in order to avoid           # race conditions on updating user.info           {:ok, _actor} <- increase_note_count_if_public(actor, activity), @@ -477,6 +489,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do        if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public      from(activity in Activity) +    |> maybe_preload_objects(opts)      |> restrict_blocked(opts)      |> restrict_recipients(recipients, opts["user"])      |> where( @@ -489,6 +502,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do          ^context        )      ) +    |> exclude_poll_votes(opts)      |> order_by([activity], desc: activity.id)    end @@ -496,7 +510,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    def fetch_activities_for_context(context, opts \\ %{}) do      context      |> fetch_activities_for_context_query(opts) -    |> Activity.with_preloaded_object()      |> Repo.all()    end @@ -504,7 +517,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do            Pleroma.FlakeId.t() | nil    def fetch_latest_activity_id_for_context(context, opts \\ %{}) do      context -    |> fetch_activities_for_context_query(opts) +    |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))      |> limit(1)      |> select([a], a.id)      |> Repo.one() @@ -803,6 +816,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    defp restrict_muted_reblogs(query, _), do: query +  defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query + +  defp exclude_poll_votes(query, _) do +    if has_named_binding?(query, :object) do +      from([activity, object: o] in query, +        where: fragment("not(?->>'type' = ?)", o.data, "Answer") +      ) +    else +      query +    end +  end +    defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query    defp maybe_preload_objects(query, _) do @@ -864,6 +889,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      |> restrict_pinned(opts)      |> restrict_muted_reblogs(opts)      |> Activity.restrict_deactivated_users() +    |> exclude_poll_votes(opts)    end    def fetch_activities(recipients, opts \\ %{}) do diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 3bf7955f3..10ceef715 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -5,8 +5,8 @@  defmodule Pleroma.Web.ActivityPub.MRF do    @callback filter(Map.t()) :: {:ok | :reject, Map.t()} -  def filter(object) do -    get_policies() +  def filter(policies, %{} = object) do +    policies      |> Enum.reduce({:ok, object}, fn        policy, {:ok, object} ->          policy.filter(object) @@ -16,6 +16,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do      end)    end +  def filter(%{} = object), do: get_policies() |> filter(object) +    def get_policies do      Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies()    end diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex new file mode 100644 index 000000000..765704389 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.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.Web.ActivityPub.MRF.SubchainPolicy do +  alias Pleroma.Config +  alias Pleroma.Web.ActivityPub.MRF + +  require Logger + +  @behaviour MRF + +  defp lookup_subchain(actor) do +    with matches <- Config.get([:mrf_subchain, :match_actor]), +         {match, subchain} <- Enum.find(matches, fn {k, _v} -> String.match?(actor, k) end) do +      {:ok, match, subchain} +    else +      _e -> {:error, :notfound} +    end +  end + +  @impl true +  def filter(%{"actor" => actor} = message) do +    with {:ok, match, subchain} <- lookup_subchain(actor) do +      Logger.debug( +        "[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{ +          inspect(subchain) +        }" +      ) + +      subchain +      |> MRF.filter(message) +    else +      _e -> {:ok, message} +    end +  end + +  @impl true +  def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index f18ffdee6..66fa7c0b3 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -35,6 +35,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> fix_likes      |> fix_addressing      |> fix_summary +    |> fix_type    end    def fix_summary(%{"summary" => nil} = object) do @@ -335,6 +336,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    def fix_content_map(object), do: object +  def fix_type(%{"inReplyTo" => reply_id} = object) when is_binary(reply_id) do +    reply = Object.normalize(reply_id) + +    if reply.data["type"] == "Question" and object["name"] do +      Map.put(object, "type", "Answer") +    else +      object +    end +  end + +  def fix_type(object), do: object +    defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do      with true <- id =~ "follows",           %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), @@ -405,7 +418,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do    # - tags    # - emoji    def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) -      when objtype in ["Article", "Note", "Video", "Page"] do +      when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do      actor = Containment.get_actor(data)      data = @@ -738,6 +751,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      |> set_reply_to_uri      |> strip_internal_fields      |> strip_internal_tags +    |> set_type    end    #  @doc @@ -902,6 +916,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do      Map.put(object, "sensitive", "nsfw" in tags)    end +  def set_type(%{"type" => "Answer"} = object) do +    Map.put(object, "type", "Note") +  end + +  def set_type(object), do: object +    def add_attributed_to(object) do      attributed_to = object["attributedTo"] || object["actor"] diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index ca8a0844b..b292d7d8d 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do    require Logger -  @supported_object_types ["Article", "Note", "Video", "Page"] +  @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]    @supported_report_states ~w(open closed resolved)    @valid_visibilities ~w(public unlisted private direct) @@ -789,4 +789,21 @@ defmodule Pleroma.Web.ActivityPub.Utils do          [to, cc, recipients]      end    end + +  def get_existing_votes(actor, %{data: %{"id" => id}}) do +    query = +      from( +        [activity, object: object] in Activity.with_preloaded_object(Activity), +        where: fragment("(?)->>'actor' = ?", activity.data, ^actor), +        where: +          fragment( +            "(?)->'inReplyTo' = ?", +            object.data, +            ^to_string(id) +          ), +        where: fragment("(?)->>'type' = 'Answer'", object.data) +      ) + +    Repo.all(query) +  end  end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 5a312d673..5212d5ce5 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -119,6 +119,53 @@ defmodule Pleroma.Web.CommonAPI do      end    end +  def vote(user, object, choices) do +    with "Question" <- object.data["type"], +         {:author, false} <- {:author, object.data["actor"] == user.ap_id}, +         {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)}, +         {options, max_count} <- get_options_and_max_count(object), +         option_count <- Enum.count(options), +         {:choice_check, {choices, true}} <- +           {:choice_check, normalize_and_validate_choice_indices(choices, option_count)}, +         {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do +      answer_activities = +        Enum.map(choices, fn index -> +          answer_data = make_answer_data(user, object, Enum.at(options, index)["name"]) + +          ActivityPub.create(%{ +            to: answer_data["to"], +            actor: user, +            context: object.data["context"], +            object: answer_data, +            additional: %{"cc" => answer_data["cc"]} +          }) +        end) + +      object = Object.get_cached_by_ap_id(object.data["id"]) +      {:ok, answer_activities, object} +    else +      {:author, _} -> {:error, "Poll's author can't vote"} +      {:existing_votes, _} -> {:error, "Already voted"} +      {:choice_check, {_, false}} -> {:error, "Invalid indices"} +      {:count_check, false} -> {:error, "Too many choices"} +    end +  end + +  defp get_options_and_max_count(object) do +    if Map.has_key?(object.data, "anyOf") do +      {object.data["anyOf"], Enum.count(object.data["anyOf"])} +    else +      {object.data["oneOf"], 1} +    end +  end + +  defp normalize_and_validate_choice_indices(choices, count) do +    Enum.map_reduce(choices, true, fn index, valid -> +      index = if is_binary(index), do: String.to_integer(index), else: index +      {index, if(valid, do: index < count, else: valid)} +    end) +  end +    def get_visibility(%{"visibility" => visibility}, in_reply_to)        when visibility in ~w{public unlisted private direct},        do: {visibility, get_replied_to_visibility(in_reply_to)} @@ -154,6 +201,7 @@ defmodule Pleroma.Web.CommonAPI do               data,               visibility             ), +         {poll, poll_emoji} <- make_poll_data(data),           {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),           context <- make_context(in_reply_to),           cw <- data["spoiler_text"] || "", @@ -171,13 +219,14 @@ defmodule Pleroma.Web.CommonAPI do               tags,               cw,               cc, -             sensitive +             sensitive, +             poll             ),           object <-             Map.put(               object,               "emoji", -             Formatter.get_emoji_map(full_payload) +             Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)             ) do        res =          ActivityPub.create( diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index d93c0d46e..f35ed36ab 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -102,6 +102,72 @@ defmodule Pleroma.Web.CommonAPI.Utils do      end    end +  def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) +      when is_list(options) do +    %{max_expiration: max_expiration, min_expiration: min_expiration} = +      limits = Pleroma.Config.get([:instance, :poll_limits]) + +    # XXX: There is probably a cleaner way of doing this +    try do +      # In some cases mastofe sends out strings instead of integers +      expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in + +      if Enum.count(options) > limits.max_options do +        raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options" +      end + +      {poll, emoji} = +        Enum.map_reduce(options, %{}, fn option, emoji -> +          if String.length(option) > limits.max_option_chars do +            raise ArgumentError, +              message: +                "Poll options cannot be longer than #{limits.max_option_chars} characters each" +          end + +          {%{ +             "name" => option, +             "type" => "Note", +             "replies" => %{"type" => "Collection", "totalItems" => 0} +           }, Map.merge(emoji, Formatter.get_emoji_map(option))} +        end) + +      case expires_in do +        expires_in when expires_in > max_expiration -> +          raise ArgumentError, message: "Expiration date is too far in the future" + +        expires_in when expires_in < min_expiration -> +          raise ArgumentError, message: "Expiration date is too soon" + +        _ -> +          :noop +      end + +      end_time = +        NaiveDateTime.utc_now() +        |> NaiveDateTime.add(expires_in) +        |> NaiveDateTime.to_iso8601() + +      poll = +        if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do +          %{"type" => "Question", "anyOf" => poll, "closed" => end_time} +        else +          %{"type" => "Question", "oneOf" => poll, "closed" => end_time} +        end + +      {poll, emoji} +    rescue +      e in ArgumentError -> e.message +    end +  end + +  def make_poll_data(%{"poll" => poll}) when is_map(poll) do +    "Invalid poll" +  end + +  def make_poll_data(_data) do +    {%{}, %{}} +  end +    def make_content_html(          status,          attachments, @@ -224,7 +290,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do          tags,          cw \\ nil,          cc \\ [], -        sensitive \\ false +        sensitive \\ false, +        merge \\ %{}        ) do      object = %{        "type" => "Note", @@ -239,12 +306,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do        "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()      } -    with false <- is_nil(in_reply_to), -         %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do -      Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) -    else -      _ -> object -    end +    object = +      with false <- is_nil(in_reply_to), +           %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do +        Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) +      else +        _ -> object +      end + +    Map.merge(object, merge)    end    def format_naive_asctime(date) do @@ -421,4 +491,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do          {:error, "No such conversation"}      end    end + +  def make_answer_data(%User{ap_id: ap_id}, object, name) do +    %{ +      "type" => "Answer", +      "actor" => ap_id, +      "cc" => [object.data["actor"]], +      "to" => [], +      "name" => name, +      "inReplyTo" => object.data["id"] +    } +  end  end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 52eed51c1..d825555c6 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -132,6 +132,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          end)        end)        |> add_if_present(params, "default_scope", :default_scope) +      |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> +        {:ok, Map.merge(user.info.pleroma_settings_store, value)} +      end)        |> add_if_present(params, "header", :banner, fn value ->          with %Plug.Upload{} <- value,               {:ok, object} <- ActivityPub.upload(value, type: :banner) do @@ -151,7 +154,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do          CommonAPI.update(user)        end -      json(conn, AccountView.render("account.json", %{user: user, for: user})) +      json( +        conn, +        AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) +      )      else        _e ->          conn @@ -161,7 +167,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do    end    def verify_credentials(%{assigns: %{user: user}} = conn, _) do -    account = AccountView.render("account.json", %{user: user, for: user}) +    account = +      AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) +      json(conn, account)    end @@ -205,7 +213,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        languages: ["en"],        registrations: Pleroma.Config.get([:instance, :registrations_open]),        # Extra (not present in Mastodon): -      max_toot_chars: Keyword.get(instance, :limit) +      max_toot_chars: Keyword.get(instance, :limit), +      poll_limits: Keyword.get(instance, :poll_limits)      }      json(conn, response) @@ -417,6 +426,53 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      end    end +  def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do +    with %Object{} = object <- Object.get_by_id(id), +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), +         true <- Visibility.visible_for_user?(activity, user) do +      conn +      |> put_view(StatusView) +      |> try_render("poll.json", %{object: object, for: user}) +    else +      nil -> +        conn +        |> put_status(404) +        |> json(%{error: "Record not found"}) + +      false -> +        conn +        |> put_status(404) +        |> json(%{error: "Record not found"}) +    end +  end + +  def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do +    with %Object{} = object <- Object.get_by_id(id), +         true <- object.data["type"] == "Question", +         %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), +         true <- Visibility.visible_for_user?(activity, user), +         {:ok, _activities, object} <- CommonAPI.vote(user, object, choices) do +      conn +      |> put_view(StatusView) +      |> try_render("poll.json", %{object: object, for: user}) +    else +      nil -> +        conn +        |> put_status(404) +        |> json(%{error: "Record not found"}) + +      false -> +        conn +        |> put_status(404) +        |> json(%{error: "Record not found"}) + +      {:error, message} -> +        conn +        |> put_status(422) +        |> json(%{error: message}) +    end +  end +    def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do      with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do        conn @@ -480,12 +536,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do        params        |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) -    idempotency_key = -      case get_req_header(conn, "idempotency-key") do -        [key] -> key -        _ -> Ecto.UUID.generate() -      end -      scheduled_at = params["scheduled_at"]      if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do @@ -498,17 +548,40 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do      else        params = Map.drop(params, ["scheduled_at"]) -      {:ok, activity} = -        Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> -          CommonAPI.post(user, params) -        end) - -      conn -      |> put_view(StatusView) -      |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +      case get_cached_status_or_post(conn, params) do +        {:ignore, message} -> +          conn +          |> put_status(422) +          |> json(%{error: message}) + +        {:error, message} -> +          conn +          |> put_status(422) +          |> json(%{error: message}) + +        {_, activity} -> +          conn +          |> put_view(StatusView) +          |> try_render("status.json", %{activity: activity, for: user, as: :activity}) +      end      end    end +  defp get_cached_status_or_post(%{assigns: %{user: user}} = conn, params) do +    idempotency_key = +      case get_req_header(conn, "idempotency-key") do +        [key] -> key +        _ -> Ecto.UUID.generate() +      end + +    Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> +      case CommonAPI.post(user, params) do +        {:ok, activity} -> activity +        {:error, message} -> {:ignore, message} +      end +    end) +  end +    def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do      with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do        json(conn, %{}) @@ -1372,6 +1445,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do              max_toot_chars: limit,              mascot: User.get_mascot(user)["url"]            }, +          poll_limits: Config.get([:instance, :poll_limits]),            rights: %{              delete_others_notice: present?(user.info.is_moderator),              admin: present?(user.info.is_admin) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 7b7e58eac..b91726b45 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -131,6 +131,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do      |> maybe_put_role(user, opts[:for])      |> maybe_put_settings(user, opts[:for], user_info)      |> maybe_put_notification_settings(user, opts[:for]) +    |> maybe_put_settings_store(user, opts[:for], opts)    end    defp username_from_nickname(string) when is_binary(string) do @@ -153,6 +154,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do    defp maybe_put_settings(data, _, _, _), do: data +  defp maybe_put_settings_store(data, %User{info: info, id: id}, %User{id: id}, %{ +         with_pleroma_settings: true +       }) do +    data +    |> Kernel.put_in([:pleroma, :settings_store], info.pleroma_settings_store) +  end + +  defp maybe_put_settings_store(data, _, _, _), do: data +    defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do      data      |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 8e8f7cf31..af1dcf66d 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -22,9 +22,14 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do      last_status = StatusView.render("status.json", %{activity: activity, for: user}) +    # Conversations return all users except the current user. +    users = +      participation.conversation.users +      |> Enum.reject(&(&1.id == user.id)) +      accounts =        AccountView.render("accounts.json", %{ -        users: participation.conversation.users, +        users: users,          as: :user        }) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 84ab20a1c..6836d331a 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -240,6 +240,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do        spoiler_text: summary_html,        visibility: get_visibility(object),        media_attachments: attachments, +      poll: render("poll.json", %{object: object, for: opts[:for]}),        mentions: mentions,        tags: build_tags(tags),        application: %{ @@ -329,6 +330,64 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do      }    end +  def render("poll.json", %{object: object} = opts) do +    {multiple, options} = +      case object.data do +        %{"anyOf" => options} when is_list(options) -> {true, options} +        %{"oneOf" => options} when is_list(options) -> {false, options} +        _ -> {nil, nil} +      end + +    if options do +      end_time = +        (object.data["closed"] || object.data["endTime"]) +        |> NaiveDateTime.from_iso8601!() + +      expired = +        end_time +        |> NaiveDateTime.compare(NaiveDateTime.utc_now()) +        |> case do +          :lt -> true +          _ -> false +        end + +      voted = +        if opts[:for] do +          existing_votes = +            Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object) + +          existing_votes != [] or opts[:for].ap_id == object.data["actor"] +        else +          false +        end + +      {options, votes_count} = +        Enum.map_reduce(options, 0, fn %{"name" => name} = option, count -> +          current_count = option["replies"]["totalItems"] || 0 + +          {%{ +             title: HTML.strip_tags(name), +             votes_count: current_count +           }, current_count + count} +        end) + +      %{ +        # Mastodon uses separate ids for polls, but an object can't have +        # more than one poll embedded so object id is fine +        id: object.id, +        expires_at: Utils.to_masto_date(end_time), +        expired: expired, +        multiple: multiple, +        votes_count: votes_count, +        options: options, +        voted: voted, +        emojis: build_emojis(object.data["emoji"]) +      } +    else +      nil +    end +  end +    def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do      object = Object.normalize(activity) diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 59f3d4e11..57f5b61bb 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -97,6 +97,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do          "pleroma_api",          "mastodon_api",          "mastodon_api_streaming", +        "polls",          if Config.get([:media_proxy, :enabled]) do            "media_proxy"          end, @@ -149,6 +150,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do          },          staffAccounts: staff_accounts,          federation: federation_response, +        pollLimits: Config.get([:instance, :poll_limits]),          postFormats: Config.get([:instance, :allowed_post_formats]),          uploadLimits: %{            general: Config.get([:instance, :upload_limit]), diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index dd7c24b52..e699f6ae2 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -333,6 +333,8 @@ defmodule Pleroma.Web.Router do        put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)        delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) +      post("/polls/:id/votes", MastodonAPIController, :poll_vote) +        post("/media", MastodonAPIController, :upload)        put("/media/:id", MastodonAPIController, :update_media) @@ -422,6 +424,8 @@ defmodule Pleroma.Web.Router do        get("/statuses/:id", MastodonAPIController, :get_status)        get("/statuses/:id/context", MastodonAPIController, :get_context) +      get("/polls/:id", MastodonAPIController, :get_poll) +        get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)        get("/accounts/:id/followers", MastodonAPIController, :followers)        get("/accounts/:id/following", MastodonAPIController, :following) diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index 85ec4d76c..b3cf9ed11 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -63,13 +63,14 @@        .scopes-input {          display: flex; +        flex-direction: column;          margin-top: 1em;          text-align: left;          color: #89898a;        }        .scopes-input label:first-child { -        flex-basis: 40%; +        height: 2em;        }        .scopes { @@ -80,13 +81,22 @@        }        .scope { -        flex-basis: 100%;          display: flex; +        flex-basis: 100%;          height: 2em;          align-items: center;        } +      .scope:before { +        color: #b9b9ba; +        content: "✔\fe0e"; +        margin-left: 1em; +        margin-right: 1em; +      } +        [type="checkbox"] + label { +        display: none; +        cursor: pointer;          margin: 0.5em;        } @@ -95,10 +105,12 @@        }        [type="checkbox"] + label:before { +        cursor: pointer;          display: inline-block;          color: white;          background-color: #121a24;          border: 4px solid #121a24; +        box-shadow: 0px 0px 1px 0 #d8a070;          box-sizing: border-box;          width: 1.2em;          height: 1.2em; @@ -128,7 +140,8 @@          border-radius: 4px;          border: none;          padding: 10px; -        margin-top: 30px; +        margin-top: 20px; +        margin-bottom: 20px;          text-transform: uppercase;          font-size: 16px;          box-shadow: 0px 0px 2px 0px black, @@ -147,8 +160,8 @@          box-sizing: border-box;          width: 100%;          background-color: #931014; +        border: 1px solid #a06060;          border-radius: 4px; -        border: none;          padding: 10px;          margin-top: 20px;          font-weight: 500; @@ -171,12 +184,27 @@            margin-top: 0          } -        .scopes-input { -          flex-direction: column; +        .scope { +          flex-basis: 0%;          } -        .scope { -          flex-basis: 50%; +        .scope:before { +          content: ""; +          margin-left: 0em; +          margin-right: 1em; +        } + +        .scope:first-child:before { +          margin-left: 1em; +          content: "✔\fe0e"; +        } + +        .scope:after { +          content: ","; +        } + +        .scope:last-child:after { +          content: "";          }        }        .form-row { diff --git a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex index e6cfe108b..c9ec1ecbf 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex @@ -1,13 +1,19 @@  <div class="scopes-input"> -  <%= label @form, :scope, "Permissions" %> - +  <%= label @form, :scope, "The following permissions will be granted" %>    <div class="scopes">      <%= for scope <- @available_scopes do %>        <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> -      <div class="scope"> +      <%= if scope in @scopes do %> +        <div class="scope"> +          <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> +          <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> +          <%= if scope in @scopes && scope do %> +            <%= String.capitalize(scope) %> +          <% end %> +        </div> +      <% else %>          <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> -        <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> -      </div> +      <% end %>      <% end %>    </div>  </div> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex index 4bcda7300..4a0718851 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -1,7 +1,9 @@  <h2>Sign in with external provider</h2>  <%= form_for @conn, o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %> -  <%= render @view_module, "_scopes.html", Map.put(assigns, :form, f) %> +  <div style="display: none"> +    <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> +  </div>    <%= hidden_input f, :client_id, value: @client_id %>    <%= hidden_input f, :redirect_uri, value: @redirect_uri %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 3e360a52c..b17142ff8 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -6,26 +6,38 @@  <% end %>  <h2>OAuth Authorization</h2> -  <%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> -<div class="input"> -  <%= label f, :name, "Name or email" %> -  <%= text_input f, :name %> -</div> -<div class="input"> -  <%= label f, :password, "Password" %> -  <%= password_input f, :password %> -</div> -<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> +<%= if @params["registration"] in ["true", true] do %> +  <h3>This is the first time you visit! Please enter your Pleroma handle.</h3> +  <p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p> +  <div class="input"> +    <%= label f, :nickname, "Pleroma Handle" %> +    <%= text_input f, :nickname, placeholder: "lain" %> +  </div> +  <%= hidden_input f, :name, value: @params["name"] %> +  <%= hidden_input f, :password, value: @params["password"] %> +  <br> +<% else %> +  <div class="input"> +    <%= label f, :name, "Username" %> +    <%= text_input f, :name %> +  </div> +  <div class="input"> +    <%= label f, :password, "Password" %> +    <%= password_input f, :password %> +  </div> +  <%= submit "Log In" %> +  <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> +<% end %>  <%= hidden_input f, :client_id, value: @client_id %>  <%= hidden_input f, :response_type, value: @response_type %>  <%= hidden_input f, :redirect_uri, value: @redirect_uri %>  <%= hidden_input f, :state, value: @state %> -<%= submit "Authorize" %>  <% end %>  <%= if Pleroma.Config.oauth_consumer_enabled?() do %>    <%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %>  <% end %> + diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index 84875613a..8d8892068 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -122,6 +122,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do              "skip_thread_containment" => user.info.skip_thread_containment            }            |> maybe_with_activation_status(user, for_user) +          |> with_notification_settings(user, for_user)        }        |> maybe_with_user_settings(user, for_user)        |> maybe_with_role(user, for_user) @@ -133,6 +134,12 @@ defmodule Pleroma.Web.TwitterAPI.UserView do      end    end +  defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do +    Map.put(data, "notification_settings", user.info.notification_settings) +  end + +  defp with_notification_settings(data, _, _), do: data +    defp maybe_with_activation_status(data, user, %User{info: %{is_admin: true}}) do      Map.put(data, "deactivated", user.info.deactivated)    end  | 
