From 62e42b03abd2cede85e85f62c35f62a8c42e8ea1 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Wed, 15 May 2019 20:10:16 +0300
Subject: [PATCH 01/32] Handle incoming Question objects

---
 .../web/activity_pub/transmogrifier.ex        |  2 +-
 lib/pleroma/web/activity_pub/utils.ex         |  2 +-
 test/fixtures/httpoison_mock/rinpatch.json    | 64 ++++++++++++
 test/fixtures/mastodon-question-activity.json | 99 +++++++++++++++++++
 test/support/http_request_mock.ex             |  8 ++
 test/web/activity_pub/transmogrifier_test.exs | 17 ++++
 6 files changed, 190 insertions(+), 2 deletions(-)
 create mode 100644 test/fixtures/httpoison_mock/rinpatch.json
 create mode 100644 test/fixtures/mastodon-question-activity.json

diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 508f3532f..c2596cfec 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -399,7 +399,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"] do
     actor = Containment.get_actor(data)
 
     data =
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 581b9d1ab..de91fa03f 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"]
 
   # Some implementations send the actor URI as the actor field, others send the entire actor object,
   # so figure out what the actor's URI is based on what we have.
diff --git a/test/fixtures/httpoison_mock/rinpatch.json b/test/fixtures/httpoison_mock/rinpatch.json
new file mode 100644
index 000000000..59311ecb6
--- /dev/null
+++ b/test/fixtures/httpoison_mock/rinpatch.json
@@ -0,0 +1,64 @@
+{
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+      "toot": "http://joinmastodon.org/ns#",
+      "featured": {
+        "@id": "toot:featured",
+        "@type": "@id"
+      },
+      "alsoKnownAs": {
+        "@id": "as:alsoKnownAs",
+        "@type": "@id"
+      },
+      "movedTo": {
+        "@id": "as:movedTo",
+        "@type": "@id"
+      },
+      "schema": "http://schema.org#",
+      "PropertyValue": "schema:PropertyValue",
+      "value": "schema:value",
+      "Hashtag": "as:Hashtag",
+      "Emoji": "toot:Emoji",
+      "IdentityProof": "toot:IdentityProof",
+      "focalPoint": {
+        "@container": "@list",
+        "@id": "toot:focalPoint"
+      }
+    }
+  ],
+  "id": "https://mastodon.sdf.org/users/rinpatch",
+  "type": "Person",
+  "following": "https://mastodon.sdf.org/users/rinpatch/following",
+  "followers": "https://mastodon.sdf.org/users/rinpatch/followers",
+  "inbox": "https://mastodon.sdf.org/users/rinpatch/inbox",
+  "outbox": "https://mastodon.sdf.org/users/rinpatch/outbox",
+  "featured": "https://mastodon.sdf.org/users/rinpatch/collections/featured",
+  "preferredUsername": "rinpatch",
+  "name": "rinpatch",
+  "summary": "<p>umu</p>",
+  "url": "https://mastodon.sdf.org/@rinpatch",
+  "manuallyApprovesFollowers": false,
+  "publicKey": {
+    "id": "https://mastodon.sdf.org/users/rinpatch#main-key",
+    "owner": "https://mastodon.sdf.org/users/rinpatch",
+    "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1vbhYKDopb5xzfJB2TZY\n0ZvgxqdAhbSKKkQC5Q2b0ofhvueDy2AuZTnVk1/BbHNlqKlwhJUSpA6LiTZVvtcc\nMn6cmSaJJEg30gRF5GARP8FMcuq8e2jmceiW99NnUX17MQXsddSf2JFUwD0rUE8H\nBsgD7UzE9+zlA/PJOTBO7fvBEz9PTQ3r4sRMTJVFvKz2MU/U+aRNTuexRKMMPnUw\nfp6VWh1F44VWJEQOs4tOEjGiQiMQh5OfBk1w2haT3vrDbQvq23tNpUP1cRomLUtx\nEBcGKi5DMMBzE1RTVT1YUykR/zLWlA+JSmw7P6cWtsHYZovs8dgn8Po3X//6N+ng\nTQIDAQAB\n-----END PUBLIC KEY-----\n"
+  },
+  "tag": [],
+  "attachment": [],
+  "endpoints": {
+    "sharedInbox": "https://mastodon.sdf.org/inbox"
+  },
+  "icon": {
+    "type": "Image",
+    "mediaType": "image/jpeg",
+    "url": "https://mastodon.sdf.org/system/accounts/avatars/000/067/580/original/bf05521bf711b7a0.jpg?1533238802"
+  },
+  "image": {
+    "type": "Image",
+    "mediaType": "image/gif",
+    "url": "https://mastodon.sdf.org/system/accounts/headers/000/067/580/original/a99b987e798f7063.gif?1533278217"
+  }
+}
diff --git a/test/fixtures/mastodon-question-activity.json b/test/fixtures/mastodon-question-activity.json
new file mode 100644
index 000000000..ac329c7d5
--- /dev/null
+++ b/test/fixtures/mastodon-question-activity.json
@@ -0,0 +1,99 @@
+{
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    {
+      "ostatus": "http://ostatus.org#",
+      "atomUri": "ostatus:atomUri",
+      "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+      "conversation": "ostatus:conversation",
+      "sensitive": "as:sensitive",
+      "Hashtag": "as:Hashtag",
+      "toot": "http://joinmastodon.org/ns#",
+      "Emoji": "toot:Emoji",
+      "focalPoint": {
+        "@container": "@list",
+        "@id": "toot:focalPoint"
+      }
+    }
+  ],
+  "id": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304/activity",
+  "type": "Create",
+  "actor": "https://mastodon.sdf.org/users/rinpatch",
+  "published": "2019-05-10T09:03:36Z",
+  "to": [
+    "https://www.w3.org/ns/activitystreams#Public"
+  ],
+  "cc": [
+    "https://mastodon.sdf.org/users/rinpatch/followers"
+  ],
+  "object": {
+    "id": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304",
+    "type": "Question",
+    "summary": null,
+    "inReplyTo": null,
+    "published": "2019-05-10T09:03:36Z",
+    "url": "https://mastodon.sdf.org/@rinpatch/102070944809637304",
+    "attributedTo": "https://mastodon.sdf.org/users/rinpatch",
+    "to": [
+      "https://www.w3.org/ns/activitystreams#Public"
+    ],
+    "cc": [
+      "https://mastodon.sdf.org/users/rinpatch/followers"
+    ],
+    "sensitive": false,
+    "atomUri": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304",
+    "inReplyToAtomUri": null,
+    "conversation": "tag:mastodon.sdf.org,2019-05-10:objectId=15095122:objectType=Conversation",
+    "content": "<p>Why is Tenshi eating a corndog so cute?</p>",
+    "contentMap": {
+      "en": "<p>Why is Tenshi eating a corndog so cute?</p>"
+    },
+    "endTime": "2019-05-11T09:03:36Z",
+    "closed": "2019-05-11T09:03:36Z",
+    "attachment": [],
+    "tag": [],
+    "replies": {
+      "id": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304/replies",
+      "type": "Collection",
+      "first": {
+        "type": "CollectionPage",
+        "partOf": "https://mastodon.sdf.org/users/rinpatch/statuses/102070944809637304/replies",
+        "items": []
+      }
+    },
+    "oneOf": [
+      {
+        "type": "Note",
+        "name": "Dunno",
+        "replies": {
+          "type": "Collection",
+          "totalItems": 0
+        }
+      },
+      {
+        "type": "Note",
+        "name": "Everyone knows that!",
+        "replies": {
+          "type": "Collection",
+          "totalItems": 1
+        }
+      },
+      {
+        "type": "Note",
+        "name": "25 char limit is dumb",
+        "replies": {
+          "type": "Collection",
+          "totalItems": 0
+        }
+      },
+      {
+        "type": "Note",
+        "name": "I can't even fit a funny",
+        "replies": {
+          "type": "Collection",
+          "totalItems": 1
+        }
+      }
+    ]
+  }
+}
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 5b355bfe6..3064c032b 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -52,6 +52,14 @@ defmodule HttpRequestMock do
      }}
   end
 
+  def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do
+    {:ok,
+     %Tesla.Env{
+       status: 200,
+       body: File.read!("test/fixtures/httpoison_mock/rinpatch.json")
+     }}
+  end
+
   def get(
         "https://mastodon.social/.well-known/webfinger?resource=https://mastodon.social/users/emelie",
         _,
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index c24b50f8c..727abbd17 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -113,6 +113,23 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       assert Enum.at(object.data["tag"], 2) == "moo"
     end
 
+    test "it works for incoming questions" do
+      data = File.read!("test/fixtures/mastodon-question-activity.json") |> Poison.decode!()
+
+      {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
+
+      object = Object.normalize(activity)
+
+      assert Enum.all?(object.data["oneOf"], fn choice ->
+               choice["name"] in [
+                 "Dunno",
+                 "Everyone knows that!",
+                 "25 char limit is dumb",
+                 "I can't even fit a funny"
+               ]
+             end)
+    end
+
     test "it works for incoming notices with contentMap" do
       data =
         File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!()

From 642a67dd4492f31b5b9fe457e34c1589c9d70c3f Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Fri, 17 May 2019 11:44:47 +0300
Subject: [PATCH 02/32] Render polls in statuses

---
 .../web/mastodon_api/views/status_view.ex     | 52 +++++++++++++++++++
 1 file changed, 52 insertions(+)

diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index bd2372944..6337c50e8 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -232,6 +232,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: %{
@@ -321,6 +322,57 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
     }
   end
 
+  # TODO: Add tests for this view
+  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!()
+
+      votes_count = object.data["votes_count"] || 0
+
+      expired =
+        end_time
+        |> NaiveDateTime.compare(NaiveDateTime.utc_now())
+        |> case do
+          :lt -> true
+          _ -> false
+        end
+
+      options =
+        Enum.map(options, fn %{"name" => name} = option ->
+          name =
+            HTML.filter_tags(
+              name,
+              User.html_filter_policy(opts[:for])
+            )
+
+          %{title: name, votes_count: option["replies"]["votes_count"] || 0}
+        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: false,
+        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)
 

From fd920c897339b9cedea042dd6698d14380cedae7 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sat, 18 May 2019 13:29:28 +0300
Subject: [PATCH 03/32] Mastodon API: Add support for posting polls

---
 lib/pleroma/web/common_api/common_api.ex      |  4 +-
 lib/pleroma/web/common_api/utils.ex           | 64 ++++++++++++++++---
 .../mastodon_api_controller_test.exs          | 22 +++++++
 test/web/mastodon_api/status_view_test.exs    |  1 +
 4 files changed, 82 insertions(+), 9 deletions(-)

diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index b53869c75..335ae70b0 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -149,6 +149,7 @@ defmodule Pleroma.Web.CommonAPI do
              data,
              visibility
            ),
+         {poll, mentions, tags} <- make_poll_data(data, mentions, tags),
          {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
          context <- make_context(in_reply_to),
          cw <- data["spoiler_text"] || "",
@@ -164,7 +165,8 @@ defmodule Pleroma.Web.CommonAPI do
              in_reply_to,
              tags,
              cw,
-             cc
+             cc,
+             poll
            ),
          object <-
            Map.put(
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 1dfe50b40..13cdffbbd 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -102,6 +102,48 @@ defmodule Pleroma.Web.CommonAPI.Utils do
     end
   end
 
+  def make_poll_data(
+        %{"poll" => %{"options" => options, "expires_in" => expires_in}} = data,
+        mentions,
+        tags
+      )
+      when is_list(options) and is_integer(expires_in) do
+    content_type = get_content_type(data["content_type"])
+    # XXX: There is probably a more performant/cleaner way to do this
+    {poll, {mentions, tags}} =
+      Enum.map_reduce(options, {mentions, tags}, fn option, {mentions, tags} ->
+        # TODO: Custom emoji
+        {option, mentions_merge, tags_merge} = format_input(option, content_type)
+        mentions = mentions ++ mentions_merge
+        tags = tags ++ tags_merge
+
+        {%{
+           "name" => option,
+           "type" => "Note",
+           "replies" => %{"type" => "Collection", "totalItems" => 0}
+         }, {mentions, tags}}
+      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, mentions, tags}
+  end
+
+  def make_poll_data(data, mentions, tags) do
+    IO.inspect(data, label: "data")
+    {%{}, mentions, tags}
+  end
+
   def make_content_html(
         status,
         attachments,
@@ -223,8 +265,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
         in_reply_to,
         tags,
         cw \\ nil,
-        cc \\ []
+        cc \\ [],
+        merge \\ %{}
       ) do
+    IO.inspect(merge, label: "merge")
+
     object = %{
       "type" => "Note",
       "to" => to,
@@ -237,14 +282,17 @@ defmodule Pleroma.Web.CommonAPI.Utils do
       "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq()
     }
 
-    if in_reply_to do
-      in_reply_to_object = Object.normalize(in_reply_to)
+    object =
+      if in_reply_to do
+        in_reply_to_object = Object.normalize(in_reply_to)
 
-      object
-      |> Map.put("inReplyTo", in_reply_to_object.data["id"])
-    else
-      object
-    end
+        object
+        |> Map.put("inReplyTo", in_reply_to_object.data["id"])
+      else
+        object
+      end
+
+    Map.merge(object, merge)
   end
 
   def format_naive_asctime(date) do
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 505e45010..ce581c092 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -132,6 +132,28 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     refute id == third_id
   end
 
+  test "posting a poll", %{conn: conn} do
+    user = insert(:user)
+    time = NaiveDateTime.utc_now()
+
+    conn =
+      conn
+      |> assign(:user, user)
+      |> post("/api/v1/statuses", %{
+        "status" => "Who is the best girl?",
+        "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
+      })
+
+    response = json_response(conn, 200)
+
+    assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
+             title in ["Rei", "Asuka", "Misato"]
+           end)
+
+    assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
+    refute response["poll"]["expred"]
+  end
+
   test "posting a sensitive status", %{conn: conn} do
     user = insert(:user)
 
diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs
index d7c800e83..9f2ebda4e 100644
--- a/test/web/mastodon_api/status_view_test.exs
+++ b/test/web/mastodon_api/status_view_test.exs
@@ -103,6 +103,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
       muted: false,
       pinned: false,
       sensitive: false,
+      poll: nil,
       spoiler_text: HtmlSanitizeEx.basic_html(note.data["object"]["summary"]),
       visibility: "public",
       media_attachments: [],

From 1d90f9b96999f8bad3fa3e3ec58bf50c8666be4f Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sun, 19 May 2019 17:06:44 +0300
Subject: [PATCH 04/32] Remove tags/mentions/rich text from poll options
 because Mastodon and add custom emoji

---
 lib/pleroma/web/common_api/common_api.ex      |  4 +--
 lib/pleroma/web/common_api/utils.ex           | 25 ++++++-------------
 .../web/mastodon_api/views/status_view.ex     | 14 +++++------
 3 files changed, 15 insertions(+), 28 deletions(-)

diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index bc8f80389..374967a1b 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -154,7 +154,7 @@ defmodule Pleroma.Web.CommonAPI do
              data,
              visibility
            ),
-         {poll, mentions, tags} <- make_poll_data(data, mentions, tags),
+         {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"] || "",
@@ -179,7 +179,7 @@ defmodule Pleroma.Web.CommonAPI do
            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 2ea997789..cd8483c11 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -102,26 +102,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do
     end
   end
 
-  def make_poll_data(
-        %{"poll" => %{"options" => options, "expires_in" => expires_in}} = data,
-        mentions,
-        tags
-      )
+  def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
       when is_list(options) and is_integer(expires_in) do
-    content_type = get_content_type(data["content_type"])
-    # XXX: There is probably a more performant/cleaner way to do this
-    {poll, {mentions, tags}} =
-      Enum.map_reduce(options, {mentions, tags}, fn option, {mentions, tags} ->
-        # TODO: Custom emoji
-        {option, mentions_merge, tags_merge} = format_input(option, content_type)
-        mentions = mentions ++ mentions_merge
-        tags = tags ++ tags_merge
-
+    {poll, emoji} =
+      Enum.map_reduce(options, %{}, fn option, emoji ->
         {%{
            "name" => option,
            "type" => "Note",
            "replies" => %{"type" => "Collection", "totalItems" => 0}
-         }, {mentions, tags}}
+         }, Map.merge(emoji, Formatter.get_emoji_map(option))}
       end)
 
     end_time =
@@ -136,11 +125,11 @@ defmodule Pleroma.Web.CommonAPI.Utils do
         %{"type" => "Question", "oneOf" => poll, "closed" => end_time}
       end
 
-    {poll, mentions, tags}
+    {poll, emoji}
   end
 
-  def make_poll_data(_data, mentions, tags) do
-    {%{}, mentions, tags}
+  def make_poll_data(_data) do
+    {%{}, %{}}
   end
 
   def make_content_html(
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 2a5691e1f..0df8bb5c2 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -325,7 +325,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   end
 
   # TODO: Add tests for this view
-  def render("poll.json", %{object: object} = opts) do
+  def render("poll.json", %{object: object} = _opts) do
     {multiple, options} =
       case object.data do
         %{"anyOf" => options} when is_list(options) -> {true, options}
@@ -350,13 +350,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
 
       options =
         Enum.map(options, fn %{"name" => name} = option ->
-          name =
-            HTML.filter_tags(
-              name,
-              User.html_filter_policy(opts[:for])
-            )
-
-          %{title: name, votes_count: option["replies"]["votes_count"] || 0}
+          %{
+            title: HTML.strip_tags(name),
+            votes_count: option["replies"]["votes_count"] || 0
+          }
         end)
 
       %{
@@ -367,6 +364,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
         multiple: multiple,
         votes_count: votes_count,
         options: options,
+        # TODO: Actually check for a vote
         voted: false,
         emojis: build_emojis(object.data["emoji"])
       }

From 6430cb1bf78e7949cc023a30df7a8d1547c36524 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sun, 19 May 2019 17:44:18 +0300
Subject: [PATCH 05/32] Restrict poll replies from fetch queries by default

---
 lib/pleroma/web/activity_pub/activity_pub.ex | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 5c3156978..035fb75d5 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -820,6 +820,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_muted_reblogs(query, _), do: query
 
+  defp restrict_poll_replies(query, %{"include_poll_replies" => "true"}), do: query
+
+  defp restrict_poll_replies(query, _) do
+    if has_named_binding?(query, :object) do
+      from([activity, object: o] in query, where: fragment("?->'name' is null", o.data))
+    else
+      query
+    end
+  end
+
   defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
 
   defp maybe_preload_objects(query, _) do
@@ -873,6 +883,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> restrict_pinned(opts)
     |> restrict_muted_reblogs(opts)
     |> Activity.restrict_deactivated_users()
+    |> restrict_poll_replies(opts)
   end
 
   def fetch_activities(recipients, opts \\ %{}) do

From 76a7429befb2e9a819b653ff8328cc42a565c29d Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 09:13:10 +0300
Subject: [PATCH 06/32] Add poll limits to /api/v1/instance and initial state

---
 config/config.exs                                       | 6 ++++++
 docs/config.md                                          | 5 +++++
 lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 4 +++-
 test/web/mastodon_api/mastodon_api_controller_test.exs  | 3 ++-
 4 files changed, 16 insertions(+), 2 deletions(-)

diff --git a/config/config.exs b/config/config.exs
index 9a10b0ff7..47d8dfb42 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -211,6 +211,12 @@ config :pleroma, :instance,
   avatar_upload_limit: 2_000_000,
   background_upload_limit: 4_000_000,
   banner_upload_limit: 4_000_000,
+  poll_limits: %{
+    max_options: 20,
+    max_option_chars: 200,
+    min_expiration: 0,
+    max_expiration: 365 * 24 * 60 * 60
+  },
   registrations_open: true,
   federating: true,
   federation_reachability_timeout_days: 7,
diff --git a/docs/config.md b/docs/config.md
index 450d73fda..f9903332c 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -71,6 +71,11 @@ config :pleroma, Pleroma.Emails.Mailer,
 * `avatar_upload_limit`: File size limit of user’s profile avatars
 * `background_upload_limit`: File size limit of user’s profile backgrounds
 * `banner_upload_limit`: File size limit of user’s profile banners
+* `poll_limits`: A map with poll limits for **local** polls
+  * `max_options`: Maximum number of options
+  * `max_option_chars`: Maximum number of characters per option
+  * `min_expiration`: Minimum expiration time (in seconds)
+  * `max_expiration`: Maximum expiration time (in seconds)
 * `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
 * `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
 * `account_activation_required`: Require users to confirm their emails before signing in.
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 1051861ff..81cc5a972 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)
@@ -1331,6 +1332,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
             max_toot_chars: limit,
             mascot: "/images/pleroma-fox-tan-smol.png"
           },
+          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/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 68fe9c1b4..48268d4f7 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -2494,7 +2494,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
              "stats" => _,
              "thumbnail" => _,
              "languages" => _,
-             "registrations" => _
+             "registrations" => _,
+             "poll_limits" => _
            } = result
 
     assert email == from_config_email

From 3f96b3e4b8114ec1cf924d452907b17c2aea2003 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 10:54:20 +0300
Subject: [PATCH 07/32] Enforce poll limits and add error handling for
 MastodonAPI's post endpoint

---
 lib/pleroma/web/common_api/utils.ex           |  71 ++++++++----
 .../mastodon_api/mastodon_api_controller.ex   |  43 ++++---
 .../mastodon_api_controller_test.exs          | 107 +++++++++++++++---
 3 files changed, 173 insertions(+), 48 deletions(-)

diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index cd8483c11..97172fd94 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -104,28 +104,61 @@ defmodule Pleroma.Web.CommonAPI.Utils do
 
   def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
       when is_list(options) and is_integer(expires_in) do
-    {poll, emoji} =
-      Enum.map_reduce(options, %{}, fn option, emoji ->
-        {%{
-           "name" => option,
-           "type" => "Note",
-           "replies" => %{"type" => "Collection", "totalItems" => 0}
-         }, Map.merge(emoji, Formatter.get_emoji_map(option))}
-      end)
+    %{max_expiration: max_expiration, min_expiration: min_expiration} =
+      limits = Pleroma.Config.get([:instance, :poll_limits])
 
-    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}
+    # XXX: There is probably a cleaner way of doing this
+    try do
+      if Enum.count(options) > limits.max_options do
+        raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options"
       end
 
-    {poll, emoji}
+      {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" => _}) do
+    "Invalid poll"
   end
 
   def make_poll_data(_data) do
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 81cc5a972..e2cab86f1 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -473,12 +473,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
@@ -491,17 +485,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)
+      case get_cached_status_or_post(conn, params) do
+        {:ignore, message} ->
+          conn
+          |> put_status(401)
+          |> json(%{error: message})
 
-      conn
-      |> put_view(StatusView)
-      |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+        {:error, message} ->
+          conn
+          |> put_status(401)
+          |> 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, %{})
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 48268d4f7..e1df79ffb 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -146,26 +146,101 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
     refute id == third_id
   end
 
-  test "posting a poll", %{conn: conn} do
-    user = insert(:user)
-    time = NaiveDateTime.utc_now()
+  describe "posting polls" do
+    test "posting a poll", %{conn: conn} do
+      user = insert(:user)
+      time = NaiveDateTime.utc_now()
 
-    conn =
-      conn
-      |> assign(:user, user)
-      |> post("/api/v1/statuses", %{
-        "status" => "Who is the best girl?",
-        "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
-      })
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses", %{
+          "status" => "Who is the #bestgrill?",
+          "poll" => %{"options" => ["Rei", "Asuka", "Misato"], "expires_in" => 420}
+        })
 
-    response = json_response(conn, 200)
+      response = json_response(conn, 200)
 
-    assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
-             title in ["Rei", "Asuka", "Misato"]
-           end)
+      assert Enum.all?(response["poll"]["options"], fn %{"title" => title} ->
+               title in ["Rei", "Asuka", "Misato"]
+             end)
 
-    assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
-    refute response["poll"]["expred"]
+      assert NaiveDateTime.diff(NaiveDateTime.from_iso8601!(response["poll"]["expires_at"]), time) in 420..430
+      refute response["poll"]["expred"]
+    end
+
+    test "option limit is enforced", %{conn: conn} do
+      user = insert(:user)
+      limit = Pleroma.Config.get([:instance, :poll_limits, :max_options])
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses", %{
+          "status" => "desu~",
+          "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
+        })
+
+      %{"error" => error} = json_response(conn, 401)
+      assert error == "Poll can't contain more than #{limit} options"
+    end
+
+    test "option character limit is enforced", %{conn: conn} do
+      user = insert(:user)
+      limit = Pleroma.Config.get([:instance, :poll_limits, :max_option_chars])
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses", %{
+          "status" => "...",
+          "poll" => %{
+            "options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)],
+            "expires_in" => 1
+          }
+        })
+
+      %{"error" => error} = json_response(conn, 401)
+      assert error == "Poll options cannot be longer than #{limit} characters each"
+    end
+
+    test "minimal date limit is enforced", %{conn: conn} do
+      user = insert(:user)
+      limit = Pleroma.Config.get([:instance, :poll_limits, :min_expiration])
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses", %{
+          "status" => "imagine arbitrary limits",
+          "poll" => %{
+            "options" => ["this post was made by pleroma gang"],
+            "expires_in" => limit - 1
+          }
+        })
+
+      %{"error" => error} = json_response(conn, 401)
+      assert error == "Expiration date is too soon"
+    end
+
+    test "maximum date limit is enforced", %{conn: conn} do
+      user = insert(:user)
+      limit = Pleroma.Config.get([:instance, :poll_limits, :max_expiration])
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> post("/api/v1/statuses", %{
+          "status" => "imagine arbitrary limits",
+          "poll" => %{
+            "options" => ["this post was made by pleroma gang"],
+            "expires_in" => limit + 1
+          }
+        })
+
+      %{"error" => error} = json_response(conn, 401)
+      assert error == "Expiration date is too far in the future"
+    end
   end
 
   test "posting a sensitive status", %{conn: conn} do

From aafe30d94e68ccf251c56139d07bda154dde3af9 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 14:12:10 +0300
Subject: [PATCH 08/32] Handle poll votes

---
 lib/pleroma/object.ex                         | 30 ++++++++++++++++++
 lib/pleroma/web/activity_pub/activity_pub.ex  | 10 ++++++
 test/fixtures/mastodon-vote.json              | 16 ++++++++++
 test/web/activity_pub/transmogrifier_test.exs | 31 +++++++++++++++++++
 4 files changed, 87 insertions(+)
 create mode 100644 test/fixtures/mastodon-vote.json

diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 740d687a3..a0f7659eb 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -188,4 +188,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 035fb75d5..ce78d895b 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),
@@ -235,6 +244,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),
diff --git a/test/fixtures/mastodon-vote.json b/test/fixtures/mastodon-vote.json
new file mode 100644
index 000000000..c2c5f40c0
--- /dev/null
+++ b/test/fixtures/mastodon-vote.json
@@ -0,0 +1,16 @@
+{
+  "@context": "https://www.w3.org/ns/activitystreams",
+  "actor": "https://mastodon.sdf.org/users/rinpatch",
+  "id": "https://mastodon.sdf.org/users/rinpatch#votes/387/activity",
+  "nickname": "rin",
+  "object": {
+    "attributedTo": "https://mastodon.sdf.org/users/rinpatch",
+    "id": "https://mastodon.sdf.org/users/rinpatch#votes/387",
+    "inReplyTo": "https://testing.uguu.ltd/objects/9d300947-2dcb-445d-8978-9a3b4b84fa14",
+    "name": "suya..",
+    "to": "https://testing.uguu.ltd/users/rin",
+    "type": "Note"
+  },
+  "to": "https://testing.uguu.ltd/users/rin",
+  "type": "Create"
+}
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 727abbd17..32d94e3e9 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -130,6 +130,37 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
              end)
     end
 
+    test "in increments vote counters on question activities" do
+      user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "suya...",
+          "poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10}
+        })
+
+      object = Object.normalize(activity)
+
+      data =
+        File.read!("test/fixtures/mastodon-vote.json")
+        |> Poison.decode!()
+        |> Kernel.put_in(["to"], user.ap_id)
+        |> Kernel.put_in(["object", "inReplyTo"], object.data["id"])
+        |> Kernel.put_in(["object", "to"], user.ap_id)
+
+      {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
+
+      object = Object.get_by_ap_id(object.data["id"])
+
+      assert Enum.any?(
+               object.data["oneOf"],
+               fn
+                 %{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true
+                 _ -> false
+               end
+             )
+    end
+
     test "it works for incoming notices with contentMap" do
       data =
         File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Poison.decode!()

From a53d06273080525fdda332291838b0c95ed69690 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 14:19:03 +0300
Subject: [PATCH 09/32] Fix posting non-polls from mastofe

---
 lib/pleroma/web/common_api/utils.ex | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 97172fd94..66153a105 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -157,7 +157,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
     end
   end
 
-  def make_poll_data(%{"poll" => _}) do
+  def make_poll_data(%{"poll" => poll}) when is_map(poll) do
     "Invalid poll"
   end
 

From f28747858bf3265e8b82eb587919f5a89386bed7 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 14:27:09 +0300
Subject: [PATCH 10/32] Actual vote count in poll view

---
 .../web/mastodon_api/views/status_view.ex        | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 0df8bb5c2..c501c213c 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -338,8 +338,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
         (object.data["closed"] || object.data["endTime"])
         |> NaiveDateTime.from_iso8601!()
 
-      votes_count = object.data["votes_count"] || 0
-
       expired =
         end_time
         |> NaiveDateTime.compare(NaiveDateTime.utc_now())
@@ -348,12 +346,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
           _ -> false
         end
 
-      options =
-        Enum.map(options, fn %{"name" => name} = option ->
-          %{
-            title: HTML.strip_tags(name),
-            votes_count: option["replies"]["votes_count"] || 0
-          }
+      {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)
 
       %{

From d7c4d029c82c833278b3640d585235dc704f30d1 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 14:35:20 +0300
Subject: [PATCH 11/32] Restrict poll replies when fetching activiites for
 context

---
 lib/pleroma/web/activity_pub/activity_pub.ex | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index ce78d895b..c04e6c2b8 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -491,6 +491,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
     from(activity in Activity)
     |> restrict_blocked(opts)
+    |> restrict_poll_replies(opts)
     |> restrict_recipients(recipients, opts["user"])
     |> where(
       [activity],

From ee682441415d7abe4acb2643e1d76fe8d78e80c1 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 16:58:15 +0300
Subject: [PATCH 12/32] Do not stream out poll replies

---
 lib/pleroma/web/activity_pub/activity_pub.ex | 54 ++++++++++----------
 1 file changed, 28 insertions(+), 26 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index c04e6c2b8..fdebf1f6b 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -192,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["name"] 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.local do
+            Pleroma.Web.Streamer.stream("public:local", activity)
+          end
 
-        if activity.data["type"] in ["Create"] do
-          object = Object.normalize(activity)
+          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)
 
-          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

From 0407ffe75f7e91db240d491492eadf1385b1726b Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 17:12:38 +0300
Subject: [PATCH 13/32] Change validation error status codes to be more
 appropriate

---
 lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 4 ++--
 test/web/mastodon_api/mastodon_api_controller_test.exs  | 8 ++++----
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index e2cab86f1..aef2abf0b 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -488,12 +488,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
       case get_cached_status_or_post(conn, params) do
         {:ignore, message} ->
           conn
-          |> put_status(401)
+          |> put_status(422)
           |> json(%{error: message})
 
         {:error, message} ->
           conn
-          |> put_status(401)
+          |> put_status(422)
           |> json(%{error: message})
 
         {_, activity} ->
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index e1df79ffb..4f332f83c 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -181,7 +181,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
           "poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1}
         })
 
-      %{"error" => error} = json_response(conn, 401)
+      %{"error" => error} = json_response(conn, 422)
       assert error == "Poll can't contain more than #{limit} options"
     end
 
@@ -200,7 +200,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
           }
         })
 
-      %{"error" => error} = json_response(conn, 401)
+      %{"error" => error} = json_response(conn, 422)
       assert error == "Poll options cannot be longer than #{limit} characters each"
     end
 
@@ -219,7 +219,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
           }
         })
 
-      %{"error" => error} = json_response(conn, 401)
+      %{"error" => error} = json_response(conn, 422)
       assert error == "Expiration date is too soon"
     end
 
@@ -238,7 +238,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
           }
         })
 
-      %{"error" => error} = json_response(conn, 401)
+      %{"error" => error} = json_response(conn, 422)
       assert error == "Expiration date is too far in the future"
     end
   end

From 5f67c26baf2bdc98480fa4cda5895f33351b13ab Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 17:30:51 +0300
Subject: [PATCH 14/32] Accept strings in expires_in because sasuga javascript

---
 lib/pleroma/web/common_api/utils.ex | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 66153a105..1a239de97 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -103,12 +103,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do
   end
 
   def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
-      when is_list(options) and is_integer(expires_in) do
+      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

From ff61d345020b9ee79b243fb8d23a37a06cc41b8e Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 17:33:54 +0300
Subject: [PATCH 15/32] Accept question objects for conversations

---
 lib/pleroma/conversation.ex | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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)
 

From 63b0b7190cb652cd27d236e3f9daaaf5e50701a6 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Tue, 21 May 2019 20:40:35 +0300
Subject: [PATCH 16/32] MastoAPI: Add GET /api/v1/polls/:id

---
 lib/pleroma/object.ex                         |  3 ++
 .../mastodon_api/mastodon_api_controller.ex   | 20 +++++++++
 lib/pleroma/web/router.ex                     |  2 +
 .../mastodon_api_controller_test.exs          | 44 +++++++++++++++++++
 4 files changed, 69 insertions(+)

diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index a0f7659eb..3ffa290eb 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
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index aef2abf0b..ecb7df459 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -410,6 +410,26 @@ 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 scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
     with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
       conn
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 6a4e4a1d4..e0611e3fc 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -423,6 +423,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/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 4f332f83c..0d56b6ff2 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -3453,4 +3453,48 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       assert json_response(conn, 403) == %{"error" => "Rate limit exceeded."}
     end
   end
+
+  describe "GET /api/v1/polls/:id" do
+    test "returns poll entity for object id", %{conn: conn} do
+      user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "Pleroma does",
+          "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20}
+        })
+
+      object = Object.normalize(activity)
+
+      conn =
+        conn
+        |> assign(:user, user)
+        |> get("/api/v1/polls/#{object.id}")
+
+      response = json_response(conn, 200)
+      id = object.id
+      assert %{"id" => ^id, "expired" => false, "multiple" => false} = response
+    end
+
+    test "does not expose polls for private statuses", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "Pleroma does",
+          "poll" => %{"options" => ["what Mastodon't", "n't what Mastodoes"], "expires_in" => 20},
+          "visibility" => "private"
+        })
+
+      object = Object.normalize(activity)
+
+      conn =
+        conn
+        |> assign(:user, other_user)
+        |> get("/api/v1/polls/#{object.id}")
+
+      assert json_response(conn, 404)
+    end
+  end
 end

From 10ca1f91de12c3ff3a1adff2cf36e9ec47c659dd Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Wed, 22 May 2019 11:56:53 +0300
Subject: [PATCH 17/32] Add GIN index on object data->'name'

---
 .../migrations/20190522084924_add_object_index_on_name.exs | 7 +++++++
 1 file changed, 7 insertions(+)
 create mode 100644 priv/repo/migrations/20190522084924_add_object_index_on_name.exs

diff --git a/priv/repo/migrations/20190522084924_add_object_index_on_name.exs b/priv/repo/migrations/20190522084924_add_object_index_on_name.exs
new file mode 100644
index 000000000..886e8499e
--- /dev/null
+++ b/priv/repo/migrations/20190522084924_add_object_index_on_name.exs
@@ -0,0 +1,7 @@
+defmodule Pleroma.Repo.Migrations.AddObjectIndexOnName do
+  use Ecto.Migration
+
+  def change do
+    create(index(:objects, ["(data->'name')"], name: :objects_name_index, using: :gin))
+  end
+end

From 19c90d47c4f649d0962098050f6cbc65eeb889d0 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Wed, 22 May 2019 21:17:57 +0300
Subject: [PATCH 18/32] Normalize poll votes to Answer objects

---
 lib/pleroma/web/activity_pub/activity_pub.ex  | 14 +--------
 .../web/activity_pub/transmogrifier.ex        | 22 ++++++++++++-
 lib/pleroma/web/activity_pub/utils.ex         |  2 +-
 test/web/activity_pub/transmogrifier_test.exs | 31 +++++++++++++++++--
 4 files changed, 51 insertions(+), 18 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index fdebf1f6b..5b3fb7e84 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -194,7 +194,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     if activity.data["type"] in ["Create", "Announce", "Delete"] do
       object = Object.normalize(activity)
       # Do not stream out poll replies
-      unless object.data["name"] do
+      unless object.data["type"] == "Answer" do
         Pleroma.Web.Streamer.stream("user", activity)
         Pleroma.Web.Streamer.stream("list", activity)
 
@@ -493,7 +493,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
     from(activity in Activity)
     |> restrict_blocked(opts)
-    |> restrict_poll_replies(opts)
     |> restrict_recipients(recipients, opts["user"])
     |> where(
       [activity],
@@ -833,16 +832,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp restrict_muted_reblogs(query, _), do: query
 
-  defp restrict_poll_replies(query, %{"include_poll_replies" => "true"}), do: query
-
-  defp restrict_poll_replies(query, _) do
-    if has_named_binding?(query, :object) do
-      from([activity, object: o] in query, where: fragment("?->'name' is null", o.data))
-    else
-      query
-    end
-  end
-
   defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query
 
   defp maybe_preload_objects(query, _) do
@@ -896,7 +885,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> restrict_pinned(opts)
     |> restrict_muted_reblogs(opts)
     |> Activity.restrict_deactivated_users()
-    |> restrict_poll_replies(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 8b2427258..70b467b3f 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
@@ -328,6 +329,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),
@@ -398,7 +411,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", "Question"] do
+      when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do
     actor = Containment.get_actor(data)
 
     data =
@@ -731,6 +744,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     |> set_reply_to_uri
     |> strip_internal_fields
     |> strip_internal_tags
+    |> set_type
   end
 
   #  @doc
@@ -895,6 +909,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 63454e3f7..9646bbee9 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", "Question"]
+  @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]
   @supported_report_states ~w(open closed resolved)
   @valid_visibilities ~w(public unlisted private direct)
 
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 32d94e3e9..8422fc3d5 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -130,7 +130,7 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
              end)
     end
 
-    test "in increments vote counters on question activities" do
+    test "it rewrites Note votes to Answers and increments vote counters on question activities" do
       user = insert(:user)
 
       {:ok, activity} =
@@ -148,8 +148,9 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
         |> Kernel.put_in(["object", "inReplyTo"], object.data["id"])
         |> Kernel.put_in(["object", "to"], user.ap_id)
 
-      {:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
-
+      {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
+      answer_object = Object.normalize(activity)
+      assert answer_object.data["type"] == "Answer"
       object = Object.get_by_ap_id(object.data["id"])
 
       assert Enum.any?(
@@ -1257,4 +1258,28 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
       {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
     end
   end
+
+  test "Rewrites Answers to Notes" do
+    user = insert(:user)
+
+    {:ok, poll_activity} =
+      CommonAPI.post(user, %{
+        "status" => "suya...",
+        "poll" => %{"options" => ["suya", "suya.", "suya.."], "expires_in" => 10}
+      })
+
+    poll_object = Object.normalize(poll_activity)
+    # TODO: Replace with CommonAPI vote creation when implemented
+    data =
+      File.read!("test/fixtures/mastodon-vote.json")
+      |> Poison.decode!()
+      |> Kernel.put_in(["to"], user.ap_id)
+      |> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"])
+      |> Kernel.put_in(["object", "to"], user.ap_id)
+
+    {:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
+    {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+
+    assert data["object"]["type"] == "Note"
+  end
 end

From ac7702f8009bf244a9e77cba39114dbb6584739f Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Wed, 22 May 2019 21:49:19 +0300
Subject: [PATCH 19/32] Exclude Answers from fetching by default

---
 lib/pleroma/web/activity_pub/activity_pub.ex | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 5b3fb7e84..db5a4a7ee 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -492,6 +492,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
       if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
 
     from(activity in Activity)
+    |> Activity.with_preloaded_object()
+    |> exclude_poll_votes(opts)
     |> restrict_blocked(opts)
     |> restrict_recipients(recipients, opts["user"])
     |> where(
@@ -832,6 +834,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
@@ -865,6 +879,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> maybe_preload_objects(opts)
     |> maybe_preload_bookmarks(opts)
     |> maybe_order(opts)
+    |> exclude_poll_votes(opts)
     |> restrict_recipients(recipients, opts["user"])
     |> restrict_tag(opts)
     |> restrict_tag_reject(opts)

From e6b175ed6cafcd6a05120e003cfe40a04b38849f Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Wed, 22 May 2019 21:57:46 +0300
Subject: [PATCH 20/32] Fix credo issues

---
 lib/pleroma/web/mastodon_api/views/status_view.ex | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index c501c213c..7a393a817 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -357,7 +357,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
         end)
 
       %{
-        # Mastodon uses separate ids for polls, but an object can't have more than one poll embedded so object id is fine
+        # 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,

From 8b2d39c1ec8f47df8a2159c23eabdc61b983ddf0 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Thu, 23 May 2019 14:03:16 +0300
Subject: [PATCH 21/32] Change the order of preloading when fetching activities
 for context

---
 lib/pleroma/web/activity_pub/activity_pub.ex | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index db5a4a7ee..b06200cb5 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -492,7 +492,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)
-    |> Activity.with_preloaded_object()
+    |> maybe_preload_objects(opts)
     |> exclude_poll_votes(opts)
     |> restrict_blocked(opts)
     |> restrict_recipients(recipients, opts["user"])
@@ -513,7 +513,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
 
@@ -521,7 +520,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()

From 300d94c62829d0ec961f3ed6c0242dea102ab0ad Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sat, 1 Jun 2019 16:07:01 +0300
Subject: [PATCH 22/32] Add poll votes

Also in this commit by accident:
- Fix query ordering causing exclude_poll_votes to not work
- Do not create notifications for Answer objects
---
 lib/pleroma/notification.ex                   | 11 ++-
 lib/pleroma/web/activity_pub/activity_pub.ex  |  2 +-
 lib/pleroma/web/activity_pub/utils.ex         | 17 +++++
 lib/pleroma/web/common_api/common_api.ex      | 46 +++++++++++
 lib/pleroma/web/common_api/utils.ex           | 11 +++
 .../mastodon_api/mastodon_api_controller.ex   | 27 +++++++
 lib/pleroma/web/router.ex                     |  2 +
 .../mastodon_api_controller_test.exs          | 76 +++++++++++++++++++
 8 files changed, 188 insertions(+), 4 deletions(-)

diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index 844264307..80e2800ae 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)
+    object = Object.normalize(activity)
 
-    notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
-    {:ok, notifications}
+    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/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index b06200cb5..5a11dadcf 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -878,7 +878,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     |> maybe_preload_objects(opts)
     |> maybe_preload_bookmarks(opts)
     |> maybe_order(opts)
-    |> exclude_poll_votes(opts)
     |> restrict_recipients(recipients, opts["user"])
     |> restrict_tag(opts)
     |> restrict_tag_reject(opts)
@@ -899,6 +898,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/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 9646bbee9..b292d7d8d 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -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 374967a1b..f54f8a7b9 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -119,6 +119,52 @@ 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)
+
+      {:ok, answer_activities, object}
+    else
+      {:author, _} -> {:error, "Already voted"}
+      {: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)}
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 1a239de97..f35ed36ab 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -491,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 ecb7df459..13bb609e5 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -430,6 +430,33 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
     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
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index e0611e3fc..d17f58f52 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -335,6 +335,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)
 
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 0d56b6ff2..b160a4db0 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -3497,4 +3497,80 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       assert json_response(conn, 404)
     end
   end
+
+  describe "POST /api/v1/polls/:id/votes" do
+    test "votes are added to the poll", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "A very delicious sandwich",
+          "poll" => %{
+            "options" => ["Lettuce", "Grilled Bacon", "Tomato"],
+            "expires_in" => 20,
+            "multiple" => true
+          }
+        })
+
+      object = Object.normalize(activity)
+
+      conn =
+        conn
+        |> assign(:user, other_user)
+        |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1, 2]})
+
+      assert json_response(conn, 200)
+      object = Object.get_by_id(object.id)
+
+      assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => totalItems}} ->
+               totalItems == 1
+             end)
+    end
+
+    test "author can't vote", %{conn: conn} do
+      user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "Am I cute?",
+          "poll" => %{"options" => ["Yes", "No"], "expires_in" => 20}
+        })
+
+      object = Object.normalize(activity)
+
+      assert conn
+             |> assign(:user, user)
+             |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
+             |> json_response(422) == %{"error" => "Already voted"}
+
+      object = Object.get_by_id(object.id)
+
+      refute Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 1
+    end
+
+    test "does not allow multiple choices on a single-choice question", %{conn: conn} do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "The glass is",
+          "poll" => %{"options" => ["half empty", "half full"], "expires_in" => 20}
+        })
+
+      object = Object.normalize(activity)
+
+      assert conn
+             |> assign(:user, other_user)
+             |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [0, 1]})
+             |> json_response(422) == %{"error" => "Too many choices"}
+
+      object = Object.get_by_id(object.id)
+
+      refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => totalItems}} ->
+               totalItems == 1
+             end)
+    end
+  end
 end

From 444406167b050524efb016cfee78636f7f6828ca Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sat, 1 Jun 2019 21:41:23 +0300
Subject: [PATCH 23/32] Mastodon API: actually check for poll votes

---
 lib/pleroma/web/mastodon_api/views/status_view.ex | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 5bf4a6ba2..7eea0122b 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -331,7 +331,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   end
 
   # TODO: Add tests for this view
-  def render("poll.json", %{object: object} = _opts) do
+  def render("poll.json", %{object: object} = opts) do
     {multiple, options} =
       case object.data do
         %{"anyOf" => options} when is_list(options) -> {true, options}
@@ -352,6 +352,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
           _ -> 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
@@ -371,8 +381,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
         multiple: multiple,
         votes_count: votes_count,
         options: options,
-        # TODO: Actually check for a vote
-        voted: false,
+        voted: voted,
         emojis: build_emojis(object.data["emoji"])
       }
     else

From 6bc9e5c020d97edcaaa386bd2ec8e8ec57cb19af Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sat, 1 Jun 2019 21:41:49 +0300
Subject: [PATCH 24/32] Mastodon API: Refresh the object before rendering it
 after voting

---
 lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index bab6d693d..8da31161f 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -436,6 +436,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
          %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
+      object = Object.get_cached_by_ap_id(object.data["id"])
+
       conn
       |> put_view(StatusView)
       |> try_render("poll.json", %{object: object, for: user})

From cfa588e3574f3083798de6f4e0eca63f00aaf578 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sat, 1 Jun 2019 21:42:29 +0300
Subject: [PATCH 25/32] Fix Credo issues

---
 test/web/mastodon_api/mastodon_api_controller_test.exs | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 24fd319d3..9c1db37db 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -3630,8 +3630,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       assert json_response(conn, 200)
       object = Object.get_by_id(object.id)
 
-      assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => totalItems}} ->
-               totalItems == 1
+      assert Enum.all?(object.data["anyOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
+               total_items == 1
              end)
     end
 
@@ -3675,8 +3675,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
 
       object = Object.get_by_id(object.id)
 
-      refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => totalItems}} ->
-               totalItems == 1
+      refute Enum.any?(object.data["oneOf"], fn %{"replies" => %{"totalItems" => total_items}} ->
+               total_items == 1
              end)
     end
   end

From 67bcc3ccc433bd5c7b59f5bf1a45db93da93d093 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sat, 1 Jun 2019 21:49:18 +0300
Subject: [PATCH 26/32] Remove index on object name used for excluding poll
 votes (replaced by just rewriting Note to Answer)

---
 .../migrations/20190522084924_add_object_index_on_name.exs | 7 -------
 1 file changed, 7 deletions(-)
 delete mode 100644 priv/repo/migrations/20190522084924_add_object_index_on_name.exs

diff --git a/priv/repo/migrations/20190522084924_add_object_index_on_name.exs b/priv/repo/migrations/20190522084924_add_object_index_on_name.exs
deleted file mode 100644
index 886e8499e..000000000
--- a/priv/repo/migrations/20190522084924_add_object_index_on_name.exs
+++ /dev/null
@@ -1,7 +0,0 @@
-defmodule Pleroma.Repo.Migrations.AddObjectIndexOnName do
-  use Ecto.Migration
-
-  def change do
-    create(index(:objects, ["(data->'name')"], name: :objects_name_index, using: :gin))
-  end
-end

From e3c460353dacb796a867a0a3afa3632746d57aa2 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sun, 2 Jun 2019 23:24:48 +0300
Subject: [PATCH 27/32] Refresh the object in CommonAPI.vote instead of
 MastoAPI controller

---
 lib/pleroma/web/common_api/common_api.ex                | 1 +
 lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 2 --
 2 files changed, 1 insertion(+), 2 deletions(-)

diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index f54f8a7b9..a12ee011b 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -141,6 +141,7 @@ defmodule Pleroma.Web.CommonAPI do
           })
         end)
 
+      object = Object.get_cached_by_ap_id(object.data["id"])
       {:ok, answer_activities, object}
     else
       {:author, _} -> {:error, "Already voted"}
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index 8da31161f..bab6d693d 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -436,8 +436,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
          %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
-      object = Object.get_cached_by_ap_id(object.data["id"])
-
       conn
       |> put_view(StatusView)
       |> try_render("poll.json", %{object: object, for: user})

From c47da0e65da32848b1f7e8cecce33a46d6f0115f Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sun, 2 Jun 2019 23:25:33 +0300
Subject: [PATCH 28/32] Add tests for poll view

---
 test/web/mastodon_api/status_view_test.exs | 102 +++++++++++++++++++++
 1 file changed, 102 insertions(+)

diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs
index 9f2ebda4e..ec75150ab 100644
--- a/test/web/mastodon_api/status_view_test.exs
+++ b/test/web/mastodon_api/status_view_test.exs
@@ -342,4 +342,106 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
         StatusView.render("card.json", %{page_url: page_url, rich_media: card})
     end
   end
+
+  describe "poll view" do
+    test "renders a poll" do
+      user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "Is Tenshi eating a corndog cute?",
+          "poll" => %{
+            "options" => ["absolutely!", "sure", "yes", "why are you even asking?"],
+            "expires_in" => 20
+          }
+        })
+
+      object = Object.normalize(activity)
+
+      expected = %{
+        emojis: [],
+        expired: false,
+        id: object.id,
+        multiple: false,
+        options: [
+          %{title: "absolutely!", votes_count: 0},
+          %{title: "sure", votes_count: 0},
+          %{title: "yes", votes_count: 0},
+          %{title: "why are you even asking?", votes_count: 0}
+        ],
+        voted: false,
+        votes_count: 0
+      }
+
+      result = StatusView.render("poll.json", %{object: object})
+      expires_at = result.expires_at
+      result = Map.delete(result, :expires_at)
+
+      assert result == expected
+
+      expires_at = NaiveDateTime.from_iso8601!(expires_at)
+      assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20
+    end
+
+    test "detects if it is multiple choice" do
+      user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "Which Mastodon developer is your favourite?",
+          "poll" => %{
+            "options" => ["Gargron", "Eugen"],
+            "expires_in" => 20,
+            "multiple" => true
+          }
+        })
+
+      object = Object.normalize(activity)
+
+      assert %{multiple: true} = StatusView.render("poll.json", %{object: object})
+    end
+
+    test "detects emoji" do
+      user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "What's with the smug face?",
+          "poll" => %{
+            "options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"],
+            "expires_in" => 20
+          }
+        })
+
+      object = Object.normalize(activity)
+
+      assert %{emojis: [%{shortcode: "blank"}]} =
+               StatusView.render("poll.json", %{object: object})
+    end
+
+    test "detects vote status" do
+      user = insert(:user)
+      other_user = insert(:user)
+
+      {:ok, activity} =
+        CommonAPI.post(user, %{
+          "status" => "Which input devices do you use?",
+          "poll" => %{
+            "options" => ["mouse", "trackball", "trackpoint"],
+            "multiple" => true,
+            "expires_in" => 20
+          }
+        })
+
+      object = Object.normalize(activity)
+
+      {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2])
+
+      result = StatusView.render("poll.json", %{object: object, for: other_user})
+
+      assert result[:voted] == true
+      assert Enum.at(result[:options], 1)[:votes_count] == 1
+      assert Enum.at(result[:options], 2)[:votes_count] == 1
+    end
+  end
 end

From 2fe3a20638789a8fb6e1a8e63cd5eb2247a9308a Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sun, 2 Jun 2019 23:30:36 +0300
Subject: [PATCH 29/32] Make error message about author's inability to vote
 more sensible

---
 lib/pleroma/web/common_api/common_api.ex               | 2 +-
 test/web/mastodon_api/mastodon_api_controller_test.exs | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index a12ee011b..5212d5ce5 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -144,7 +144,7 @@ defmodule Pleroma.Web.CommonAPI do
       object = Object.get_cached_by_ap_id(object.data["id"])
       {:ok, answer_activities, object}
     else
-      {:author, _} -> {:error, "Already voted"}
+      {: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"}
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 9c1db37db..c158c4df2 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -3649,7 +3649,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
       assert conn
              |> assign(:user, user)
              |> post("/api/v1/polls/#{object.id}/votes", %{"choices" => [1]})
-             |> json_response(422) == %{"error" => "Already voted"}
+             |> json_response(422) == %{"error" => "Poll's author can't vote"}
 
       object = Object.get_by_id(object.id)
 

From 1fd8e19d767e03475555e879c8e5e0fcc8d5c6bf Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Sun, 2 Jun 2019 23:46:17 +0300
Subject: [PATCH 30/32] Remove a TODO comment as the tests for poll view were
 written

---
 lib/pleroma/web/mastodon_api/views/status_view.ex | 1 -
 1 file changed, 1 deletion(-)

diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 7eea0122b..6836d331a 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -330,7 +330,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
     }
   end
 
-  # TODO: Add tests for this view
   def render("poll.json", %{object: object} = opts) do
     {multiple, options} =
       case object.data do

From baefb97dc4f4435027f6e23e77433e4bfaf0dcd1 Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Mon, 3 Jun 2019 10:55:16 +0300
Subject: [PATCH 31/32] Add a changelog entry for polls

---
 CHANGELOG.md | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff1fff876..2144bbe28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,7 +16,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mix Tasks: `mix pleroma.database remove_embedded_objects`
 - Mix Tasks: `mix pleroma.database update_users_following_followers_counts`
 - Mix Tasks: `mix pleroma.user toggle_confirmed`
+- Federation: Support for `Question` and `Answer` objects
 - Federation: Support for reports
+- Configuration: `poll_limits` option
 - Configuration: `safe_dm_mentions` option
 - Configuration: `link_name` option
 - Configuration: `fetch_initial_posts` option
@@ -37,6 +39,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
 - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
 - Mastodon API: `POST /api/v1/accounts` (account creation API)
+- Mastodon API: [Polls](https://docs.joinmastodon.org/api/rest/polls/)
 - ActivityPub C2S: OAuth endpoints
 - Metadata: RelMe provider
 - OAuth: added support for refresh tokens

From 5bd41fef8b5aeff53ed6b096e04507d51c93a83a Mon Sep 17 00:00:00 2001
From: rinpatch <rinpatch@sdf.org>
Date: Mon, 3 Jun 2019 10:58:37 +0300
Subject: [PATCH 32/32] Change query order in
 fetch_activities_for_context_query to make poll vote exclusion work

---
 lib/pleroma/web/activity_pub/activity_pub.ex | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index d494d5cd1..45feae25a 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -489,7 +489,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
     from(activity in Activity)
     |> maybe_preload_objects(opts)
-    |> exclude_poll_votes(opts)
     |> restrict_blocked(opts)
     |> restrict_recipients(recipients, opts["user"])
     |> where(
@@ -502,6 +501,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
         ^context
       )
     )
+    |> exclude_poll_votes(opts)
     |> order_by([activity], desc: activity.id)
   end