diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/pleroma/conversation.ex | 2 | ||||
| -rw-r--r-- | lib/pleroma/notification.ex | 13 | ||||
| -rw-r--r-- | lib/pleroma/object.ex | 33 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 82 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/transmogrifier.ex | 22 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/utils.ex | 19 | ||||
| -rw-r--r-- | lib/pleroma/web/common_api/common_api.ex | 53 | ||||
| -rw-r--r-- | lib/pleroma/web/common_api/utils.ex | 95 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 96 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/views/status_view.ex | 59 | ||||
| -rw-r--r-- | lib/pleroma/web/router.ex | 4 | 
11 files changed, 419 insertions, 59 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 35beffb87..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, []} 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/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 8add62406..45feae25a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -108,6 +108,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), @@ -183,40 +192,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 @@ -235,6 +246,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), @@ -476,6 +488,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( @@ -488,6 +501,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do          ^context        )      ) +    |> exclude_poll_votes(opts)      |> order_by([activity], desc: activity.id)    end @@ -495,7 +509,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 @@ -503,7 +516,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() @@ -802,6 +815,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 @@ -863,6 +888,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/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 0d81fb840..dfd05271a 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -197,7 +197,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) @@ -409,6 +410,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 @@ -472,12 +520,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 @@ -490,17 +532,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, %{}) @@ -1364,6 +1429,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/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/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) | 
