diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | config/config.exs | 1 | ||||
| -rw-r--r-- | lib/pleroma/notification.ex | 81 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 7 | ||||
| -rw-r--r-- | lib/pleroma/web/activity_pub/side_effects.ex | 20 | ||||
| -rw-r--r-- | lib/pleroma/web/api_spec/operations/notification_operation.ex | 3 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/controllers/notification_controller.ex | 1 | ||||
| -rw-r--r-- | lib/pleroma/web/mastodon_api/views/notification_view.ex | 3 | ||||
| -rw-r--r-- | lib/pleroma/web/push/subscription.ex | 2 | ||||
| -rw-r--r-- | lib/pleroma/workers/poll_worker.ex | 45 | ||||
| -rw-r--r-- | priv/repo/migrations/20210717000000_add_poll_to_notifications_enum.exs | 49 | ||||
| -rw-r--r-- | test/pleroma/notification_test.exs | 13 | ||||
| -rw-r--r-- | test/pleroma/web/activity_pub/side_effects_test.exs | 24 | ||||
| -rw-r--r-- | test/pleroma/web/common_api_test.exs | 7 | ||||
| -rw-r--r-- | test/pleroma/web/mastodon_api/controllers/status_controller_test.exs | 5 | ||||
| -rw-r--r-- | test/pleroma/web/mastodon_api/views/notification_view_test.exs | 21 | ||||
| -rw-r--r-- | test/support/factory.ex | 59 | 
17 files changed, 314 insertions, 28 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e594f174..45a365505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).  - AdminAPI: return `created_at` date with users.  - `AnalyzeMetadata` upload filter for extracting image/video attachment dimensions and generating blurhashes for images. Blurhashes for videos are not generated at this time.  - Attachment dimensions and blurhashes are federated when available. +- Mastodon API: support `poll` notification.  - Pinned posts federation  ### Fixed diff --git a/config/config.exs b/config/config.exs index b50c910b1..828fe0085 100644 --- a/config/config.exs +++ b/config/config.exs @@ -560,6 +560,7 @@ config :pleroma, Oban,      mailer: 10,      transmogrifier: 20,      scheduled_activities: 10, +    poll_notifications: 10,      background: 5,      remote_fetcher: 2,      attachments_cleanup: 1, diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 7efbdc49a..32f13df69 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -72,6 +72,7 @@ defmodule Pleroma.Notification do      pleroma:emoji_reaction      pleroma:report      reblog +    poll    }    def changeset(%Notification{} = notification, attrs) do @@ -379,7 +380,7 @@ defmodule Pleroma.Notification do      notifications =        Enum.map(potential_receivers, fn user ->          do_send = do_send && user in enabled_receivers -        create_notification(activity, user, do_send) +        create_notification(activity, user, do_send: do_send)        end)        |> Enum.reject(&is_nil/1) @@ -435,15 +436,18 @@ defmodule Pleroma.Notification do    end    # TODO move to sql, too. -  def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do -    unless skip?(activity, user) do +  def create_notification(%Activity{} = activity, %User{} = user, opts \\ []) do +    do_send = Keyword.get(opts, :do_send, true) +    type = Keyword.get(opts, :type, type_from_activity(activity)) + +    unless skip?(activity, user, opts) do        {:ok, %{notification: notification}} =          Multi.new()          |> Multi.insert(:notification, %Notification{            user_id: user.id,            activity: activity,            seen: mark_as_read?(activity, user), -          type: type_from_activity(activity) +          type: type          })          |> Marker.multi_set_last_read_id(user, "notifications")          |> Repo.transaction() @@ -457,6 +461,28 @@ defmodule Pleroma.Notification do      end    end +  def create_poll_notifications(%Activity{} = activity) do +    with %Object{data: %{"type" => "Question", "actor" => actor} = data} <- +           Object.normalize(activity) do +      voters = +        case data do +          %{"voters" => voters} when is_list(voters) -> voters +          _ -> [] +        end + +      notifications = +        Enum.reduce([actor | voters], [], fn ap_id, acc -> +          with %User{local: true} = user <- User.get_by_ap_id(ap_id) do +            [create_notification(activity, user, type: "poll") | acc] +          else +            _ -> acc +          end +        end) + +      {:ok, notifications} +    end +  end +    @doc """    Returns a tuple with 2 elements:      {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)} @@ -572,8 +598,10 @@ defmodule Pleroma.Notification do      Enum.uniq(ap_ids) -- thread_muter_ap_ids    end -  @spec skip?(Activity.t(), User.t()) :: boolean() -  def skip?(%Activity{} = activity, %User{} = user) do +  def skip?(activity, user, opts \\ []) + +  @spec skip?(Activity.t(), User.t(), Keyword.t()) :: boolean() +  def skip?(%Activity{} = activity, %User{} = user, opts) do      [        :self,        :invisible, @@ -581,17 +609,21 @@ defmodule Pleroma.Notification do        :recently_followed,        :filtered      ] -    |> Enum.find(&skip?(&1, activity, user)) +    |> Enum.find(&skip?(&1, activity, user, opts))    end -  def skip?(_, _), do: false +  def skip?(_activity, _user, _opts), do: false -  @spec skip?(atom(), Activity.t(), User.t()) :: boolean() -  def skip?(:self, %Activity{} = activity, %User{} = user) do -    activity.data["actor"] == user.ap_id +  @spec skip?(atom(), Activity.t(), User.t(), Keyword.t()) :: boolean() +  def skip?(:self, %Activity{} = activity, %User{} = user, opts) do +    cond do +      opts[:type] == "poll" -> false +      activity.data["actor"] == user.ap_id -> true +      true -> false +    end    end -  def skip?(:invisible, %Activity{} = activity, _) do +  def skip?(:invisible, %Activity{} = activity, _user, _opts) do      actor = activity.data["actor"]      user = User.get_cached_by_ap_id(actor)      User.invisible?(user) @@ -600,15 +632,27 @@ defmodule Pleroma.Notification do    def skip?(          :block_from_strangers,          %Activity{} = activity, -        %User{notification_settings: %{block_from_strangers: true}} = user +        %User{notification_settings: %{block_from_strangers: true}} = user, +        opts        ) do      actor = activity.data["actor"]      follower = User.get_cached_by_ap_id(actor) -    !User.following?(follower, user) + +    cond do +      opts[:type] == "poll" -> false +      user.ap_id == actor -> false +      !User.following?(follower, user) -> true +      true -> false +    end    end    # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL -  def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do +  def skip?( +        :recently_followed, +        %Activity{data: %{"type" => "Follow"}} = activity, +        %User{} = user, +        _opts +      ) do      actor = activity.data["actor"]      Notification.for_user(user) @@ -618,9 +662,10 @@ defmodule Pleroma.Notification do      end)    end -  def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false +  def skip?(:filtered, %{data: %{"type" => type}}, _user, _opts) when type in ["Follow", "Move"], +    do: false -  def skip?(:filtered, activity, user) do +  def skip?(:filtered, activity, user, _opts) do      object = Object.normalize(activity, fetch: false)      cond do @@ -638,7 +683,7 @@ defmodule Pleroma.Notification do      end    end -  def skip?(_, _, _), do: false +  def skip?(_type, _activity, _user, _opts), do: false    def mark_as_read?(activity, target_user) do      user = Activity.user_actor(activity) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 4c29dda35..19961a4a5 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -25,6 +25,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do    alias Pleroma.Web.Streamer    alias Pleroma.Web.WebFinger    alias Pleroma.Workers.BackgroundWorker +  alias Pleroma.Workers.PollWorker    import Ecto.Query    import Pleroma.Web.ActivityPub.Utils @@ -288,6 +289,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do           {:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},           {:ok, _actor} <- increase_note_count_if_public(actor, activity),           _ <- notify_and_stream(activity), +         :ok <- maybe_schedule_poll_notifications(activity),           :ok <- maybe_federate(activity) do        {:ok, activity}      else @@ -302,6 +304,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do      end    end +  defp maybe_schedule_poll_notifications(activity) do +    PollWorker.schedule_poll_end(activity) +    :ok +  end +    @spec listen(map()) :: {:ok, Activity.t()} | {:error, any()}    def listen(%{to: to, actor: actor, context: context, object: object} = params) do      additional = params[:additional] || %{} diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index b0ec84ade..dda48ea5f 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -24,6 +24,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.Push    alias Pleroma.Web.Streamer +  alias Pleroma.Workers.PollWorker    require Logger @@ -195,7 +196,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do    # - Set up notifications    @impl true    def handle(%{data: %{"type" => "Create"}} = activity, meta) do -    with {:ok, object, meta} <- handle_object_creation(meta[:object_data], meta), +    with {:ok, object, meta} <- handle_object_creation(meta[:object_data], activity, meta),           %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do        {:ok, notifications} = Notification.create_notifications(activity, do_send: false)        {:ok, _user} = ActivityPub.increase_note_count_if_public(user, object) @@ -389,7 +390,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      {:ok, object, meta}    end -  def handle_object_creation(%{"type" => "ChatMessage"} = object, meta) do +  def handle_object_creation(%{"type" => "ChatMessage"} = object, _activity, meta) do      with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do        actor = User.get_cached_by_ap_id(object.data["actor"])        recipient = User.get_cached_by_ap_id(hd(object.data["to"])) @@ -424,7 +425,14 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      end    end -  def handle_object_creation(%{"type" => "Answer"} = object_map, meta) do +  def handle_object_creation(%{"type" => "Question"} = object, activity, meta) do +    with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do +      PollWorker.schedule_poll_end(activity) +      {:ok, object, meta} +    end +  end + +  def handle_object_creation(%{"type" => "Answer"} = object_map, _activity, meta) do      with {:ok, object, meta} <- Pipeline.common_pipeline(object_map, meta) do        Object.increase_vote_count(          object.data["inReplyTo"], @@ -436,15 +444,15 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do      end    end -  def handle_object_creation(%{"type" => objtype} = object, meta) -      when objtype in ~w[Audio Video Question Event Article Note Page] do +  def handle_object_creation(%{"type" => objtype} = object, _activity, meta) +      when objtype in ~w[Audio Video Event Article Note Page] do      with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do        {:ok, object, meta}      end    end    # Nothing to do -  def handle_object_creation(object, meta) do +  def handle_object_creation(object, _activity, meta) do      {:ok, object, meta}    end diff --git a/lib/pleroma/web/api_spec/operations/notification_operation.ex b/lib/pleroma/web/api_spec/operations/notification_operation.ex index ec88eabe1..e4ce42f1c 100644 --- a/lib/pleroma/web/api_spec/operations/notification_operation.ex +++ b/lib/pleroma/web/api_spec/operations/notification_operation.ex @@ -195,7 +195,8 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do          "pleroma:chat_mention",          "pleroma:report",          "move", -        "follow_request" +        "follow_request", +        "poll"        ],        description: """        The type of event that resulted in the notification. diff --git a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex index 647ba661e..002d6b2ce 100644 --- a/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/notification_controller.ex @@ -50,6 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do      favourite      move      pleroma:emoji_reaction +    poll    }    def index(%{assigns: %{user: user}} = conn, params) do      params = diff --git a/lib/pleroma/web/mastodon_api/views/notification_view.ex b/lib/pleroma/web/mastodon_api/views/notification_view.ex index df9bedfed..35c636d4e 100644 --- a/lib/pleroma/web/mastodon_api/views/notification_view.ex +++ b/lib/pleroma/web/mastodon_api/views/notification_view.ex @@ -112,6 +112,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do        "move" ->          put_target(response, activity, reading_user, %{}) +      "poll" -> +        put_status(response, activity, reading_user, status_render_opts) +        "pleroma:emoji_reaction" ->          response          |> put_status(parent_activity_fn.(), reading_user, status_render_opts) diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex index 4f6c9bc9f..35bf2e223 100644 --- a/lib/pleroma/web/push/subscription.ex +++ b/lib/pleroma/web/push/subscription.ex @@ -26,7 +26,7 @@ defmodule Pleroma.Web.Push.Subscription do    end    # credo:disable-for-next-line Credo.Check.Readability.MaxLineLength -  @supported_alert_types ~w[follow favourite mention reblog pleroma:chat_mention pleroma:emoji_reaction]a +  @supported_alert_types ~w[follow favourite mention reblog poll pleroma:chat_mention pleroma:emoji_reaction]a    defp alerts(%{data: %{alerts: alerts}}) do      alerts = Map.take(alerts, @supported_alert_types) diff --git a/lib/pleroma/workers/poll_worker.ex b/lib/pleroma/workers/poll_worker.ex new file mode 100644 index 000000000..3423cc889 --- /dev/null +++ b/lib/pleroma/workers/poll_worker.ex @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PollWorker do +  @moduledoc """ +  Generates notifications when a poll ends. +  """ +  use Pleroma.Workers.WorkerHelper, queue: "poll_notifications" + +  alias Pleroma.Activity +  alias Pleroma.Notification +  alias Pleroma.Object + +  @impl Oban.Worker +  def perform(%Job{args: %{"op" => "poll_end", "activity_id" => activity_id}}) do +    with %Activity{} = activity <- find_poll_activity(activity_id) do +      Notification.create_poll_notifications(activity) +    end +  end + +  defp find_poll_activity(activity_id) do +    with nil <- Activity.get_by_id(activity_id) do +      {:error, :poll_activity_not_found} +    end +  end + +  def schedule_poll_end(%Activity{data: %{"type" => "Create"}, id: activity_id} = activity) do +    with %Object{data: %{"type" => "Question", "closed" => closed}} when is_binary(closed) <- +           Object.normalize(activity), +         {:ok, end_time} <- NaiveDateTime.from_iso8601(closed), +         :gt <- NaiveDateTime.compare(end_time, NaiveDateTime.utc_now()) do +      %{ +        op: "poll_end", +        activity_id: activity_id +      } +      |> new(scheduled_at: end_time) +      |> Oban.insert() +    else +      _ -> {:error, activity} +    end +  end + +  def schedule_poll_end(activity), do: {:error, activity} +end diff --git a/priv/repo/migrations/20210717000000_add_poll_to_notifications_enum.exs b/priv/repo/migrations/20210717000000_add_poll_to_notifications_enum.exs new file mode 100644 index 000000000..9abf40b3d --- /dev/null +++ b/priv/repo/migrations/20210717000000_add_poll_to_notifications_enum.exs @@ -0,0 +1,49 @@ +defmodule Pleroma.Repo.Migrations.AddPollToNotificationsEnum do +  use Ecto.Migration + +  @disable_ddl_transaction true + +  def up do +    """ +    alter type notification_type add value 'poll' +    """ +    |> execute() +  end + +  def down do +    alter table(:notifications) do +      modify(:type, :string) +    end + +    """ +    delete from notifications where type = 'poll' +    """ +    |> execute() + +    """ +    drop type if exists notification_type +    """ +    |> execute() + +    """ +    create type notification_type as enum ( +      'follow', +      'follow_request', +      'mention', +      'move', +      'pleroma:emoji_reaction', +      'pleroma:chat_mention', +      'reblog', +      'favourite', +      'pleroma:report' +    ) +    """ +    |> execute() + +    """ +    alter table notifications +    alter column type type notification_type using (type::notification_type) +    """ +    |> execute() +  end +end diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs index 85f895f0f..716af496d 100644 --- a/test/pleroma/notification_test.exs +++ b/test/pleroma/notification_test.exs @@ -129,6 +129,19 @@ defmodule Pleroma.NotificationTest do      end    end +  test "create_poll_notifications/1" do +    [user1, user2, user3, _, _] = insert_list(5, :user) +    question = insert(:question, user: user1) +    activity = insert(:question_activity, question: question) + +    {:ok, _, _} = CommonAPI.vote(user2, question, [0]) +    {:ok, _, _} = CommonAPI.vote(user3, question, [1]) + +    {:ok, notifications} = Notification.create_poll_notifications(activity) + +    assert [user2.id, user3.id, user1.id] == Enum.map(notifications, & &1.user_id) +  end +    describe "CommonApi.post/2 notification-related functionality" do      test_with_mock "creates but does NOT send notification to blocker user",                     Push, diff --git a/test/pleroma/web/activity_pub/side_effects_test.exs b/test/pleroma/web/activity_pub/side_effects_test.exs index 13167f50a..d0988619d 100644 --- a/test/pleroma/web/activity_pub/side_effects_test.exs +++ b/test/pleroma/web/activity_pub/side_effects_test.exs @@ -157,6 +157,30 @@ defmodule Pleroma.Web.ActivityPub.SideEffectsTest do      end    end +  describe "Question objects" do +    setup do +      user = insert(:user) +      question = build(:question, user: user) +      question_activity = build(:question_activity, question: question) +      activity_data = Map.put(question_activity.data, "object", question.data["id"]) +      meta = [object_data: question.data, local: false] + +      {:ok, activity, meta} = ActivityPub.persist(activity_data, meta) + +      %{activity: activity, meta: meta} +    end + +    test "enqueues the poll end", %{activity: activity, meta: meta} do +      {:ok, activity, meta} = SideEffects.handle(activity, meta) + +      assert_enqueued( +        worker: Pleroma.Workers.PollWorker, +        args: %{op: "poll_end", activity_id: activity.id}, +        scheduled_at: NaiveDateTime.from_iso8601!(meta[:object_data]["closed"]) +      ) +    end +  end +    describe "delete users with confirmation pending" do      setup do        user = insert(:user, is_confirmed: false) diff --git a/test/pleroma/web/common_api_test.exs b/test/pleroma/web/common_api_test.exs index a5dfd3934..4a10a5bc4 100644 --- a/test/pleroma/web/common_api_test.exs +++ b/test/pleroma/web/common_api_test.exs @@ -18,6 +18,7 @@ defmodule Pleroma.Web.CommonAPITest do    alias Pleroma.Web.ActivityPub.Visibility    alias Pleroma.Web.AdminAPI.AccountView    alias Pleroma.Web.CommonAPI +  alias Pleroma.Workers.PollWorker    import Pleroma.Factory    import Mock @@ -48,6 +49,12 @@ defmodule Pleroma.Web.CommonAPITest do        assert object.data["type"] == "Question"        assert object.data["oneOf"] |> length() == 2 + +      assert_enqueued( +        worker: PollWorker, +        args: %{op: "poll_end", activity_id: activity.id}, +        scheduled_at: NaiveDateTime.from_iso8601!(object.data["closed"]) +      )      end    end diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index d478a81ee..ed66d370a 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do    alias Pleroma.Web.ActivityPub.ActivityPub    alias Pleroma.Web.ActivityPub.Utils    alias Pleroma.Web.CommonAPI +  alias Pleroma.Workers.ScheduledActivityWorker    import Pleroma.Factory @@ -705,11 +706,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do          |> json_response_and_validate_schema(200)        assert {:ok, %{id: activity_id}} = -               perform_job(Pleroma.Workers.ScheduledActivityWorker, %{ +               perform_job(ScheduledActivityWorker, %{                   activity_id: scheduled_id                 }) -      assert Repo.all(Oban.Job) == [] +      refute_enqueued(worker: ScheduledActivityWorker)        object =          Activity diff --git a/test/pleroma/web/mastodon_api/views/notification_view_test.exs b/test/pleroma/web/mastodon_api/views/notification_view_test.exs index 496a688d1..8070c03c9 100644 --- a/test/pleroma/web/mastodon_api/views/notification_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/notification_view_test.exs @@ -196,6 +196,27 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do      test_notifications_rendering([notification], user, [expected])    end +  test "Poll notification" do +    user = insert(:user) +    activity = insert(:question_activity, user: user) +    {:ok, [notification]} = Notification.create_poll_notifications(activity) + +    expected = %{ +      id: to_string(notification.id), +      pleroma: %{is_seen: false, is_muted: false}, +      type: "poll", +      account: +        AccountView.render("show.json", %{ +          user: user, +          for: user +        }), +      status: StatusView.render("show.json", %{activity: activity, for: user}), +      created_at: Utils.to_masto_date(notification.inserted_at) +    } + +    test_notifications_rendering([notification], user, [expected]) +  end +    test "Report notification" do      reporting_user = insert(:user)      reported_user = insert(:user) diff --git a/test/support/factory.ex b/test/support/factory.ex index f31f64a50..4a78425ce 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -213,6 +213,38 @@ defmodule Pleroma.Factory do      }    end +  def question_factory(attrs \\ %{}) do +    user = attrs[:user] || insert(:user) + +    data = %{ +      "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(), +      "type" => "Question", +      "actor" => user.ap_id, +      "attributedTo" => user.ap_id, +      "attachment" => [], +      "to" => ["https://www.w3.org/ns/activitystreams#Public"], +      "cc" => [user.follower_address], +      "context" => Pleroma.Web.ActivityPub.Utils.generate_context_id(), +      "closed" => DateTime.utc_now() |> DateTime.add(86_400) |> DateTime.to_iso8601(), +      "oneOf" => [ +        %{ +          "type" => "Note", +          "name" => "chocolate", +          "replies" => %{"totalItems" => 0, "type" => "Collection"} +        }, +        %{ +          "type" => "Note", +          "name" => "vanilla", +          "replies" => %{"totalItems" => 0, "type" => "Collection"} +        } +      ] +    } + +    %Pleroma.Object{ +      data: merge_attributes(data, Map.get(attrs, :data, %{})) +    } +  end +    def direct_note_activity_factory do      dm = insert(:direct_note) @@ -428,6 +460,33 @@ defmodule Pleroma.Factory do      }    end +  def question_activity_factory(attrs \\ %{}) do +    user = attrs[:user] || insert(:user) +    question = attrs[:question] || insert(:question, user: user) + +    data_attrs = attrs[:data_attrs] || %{} +    attrs = Map.drop(attrs, [:user, :question, :data_attrs]) + +    data = +      %{ +        "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), +        "type" => "Create", +        "actor" => question.data["actor"], +        "to" => question.data["to"], +        "object" => question.data["id"], +        "published" => DateTime.utc_now() |> DateTime.to_iso8601(), +        "context" => question.data["context"] +      } +      |> Map.merge(data_attrs) + +    %Pleroma.Activity{ +      data: data, +      actor: data["actor"], +      recipients: data["to"] +    } +    |> Map.merge(attrs) +  end +    def oauth_app_factory do      %Pleroma.Web.OAuth.App{        client_name: sequence(:client_name, &"Some client #{&1}"),  | 
