diff --git a/config/config.exs b/config/config.exs index f7455cf97..2d93cd0dd 100644 --- a/config/config.exs +++ b/config/config.exs @@ -553,7 +553,8 @@ config :pleroma, Oban, remote_fetcher: 2, attachments_cleanup: 5, new_users_digest: 1, - mute_expire: 5 + mute_expire: 5, + poll_expiration_notify: 1 ], plugins: [Oban.Plugins.Pruner], crontab: [ diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index dd7a1c824..8ee9f71c1 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -368,7 +368,7 @@ defmodule Pleroma.Notification do end def create_notifications(%Activity{data: %{"type" => type}} = activity, options) - when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag"] do + when type in ["Follow", "Like", "Announce", "Move", "EmojiReact", "Flag", "ClosePoll"] do do_create_notifications(activity, options) end @@ -418,9 +418,11 @@ defmodule Pleroma.Notification do "EmojiReaction" -> "pleroma:emoji_reaction" + "ClosePoll" -> + "poll" + "Create" -> - activity - |> type_from_activity_object() + type_from_activity_object(activity) t -> raise "No notification type for activity type #{t}" @@ -471,7 +473,16 @@ defmodule Pleroma.Notification do def get_notified_from_activity(activity, local_only \\ true) def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) - when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact", "Flag"] do + when type in [ + "Create", + "Like", + "Announce", + "Follow", + "Move", + "EmojiReact", + "Flag", + "ClosePoll" + ] do potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity) potential_receivers = @@ -511,6 +522,10 @@ defmodule Pleroma.Notification do User.all_superusers() |> Enum.map(fn user -> user.ap_id end) end + def get_potential_receiver_ap_ids(%{data: %{"type" => "ClosePoll"}}) do + [] + end + def get_potential_receiver_ap_ids(activity) do [] |> Utils.maybe_notify_to_recipients(activity) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index e99f6fd83..bb579083c 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -65,6 +65,15 @@ defmodule Pleroma.Web.ActivityPub.Builder do end end + def close_poll(_activity, object) do + {:ok, + %{ + "id" => Utils.generate_activity_id(), + "type" => "ClosePoll", + "object" => object.data["id"] + }, []} + end + @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()} def undo(actor, object) do {:ok, diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index bd0a2a8dc..aa1d64323 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -21,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.ClosePollValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.CreateGenericValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator @@ -151,6 +152,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do end end + def validate(%{"type" => "ClosePoll"} = object, meta) do + with {:ok, object} <- + object + |> ClosePollValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + def validate(%{"type" => type} = object, meta) when type in ~w[Audio Video] do with {:ok, object} <- object diff --git a/lib/pleroma/web/activity_pub/object_validators/close_poll_validator.ex b/lib/pleroma/web/activity_pub/object_validators/close_poll_validator.ex new file mode 100644 index 000000000..c3325948e --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/close_poll_validator.ex @@ -0,0 +1,41 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.ClosePollValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + + import Ecto.Changeset + + @primary_key false + + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:type, :string) + field(:object, ObjectValidators.ObjectID) + end + + def cast_and_validate(data) do + data + |> cast_data() + |> validate_data() + end + + def cast_data(data) do + %__MODULE__{} + |> changeset(data) + end + + def changeset(struct, data) do + struct + |> cast(data, __schema__(:fields)) + end + + def validate_data(data_cng) do + data_cng + |> validate_inclusion(:type, ["ClosePoll"]) + |> validate_required([:id, :type, :object]) + end +end diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 4d8fb721e..d8ef04a62 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -245,6 +245,14 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do end # Tasks this handles: + # - Set up notification on close poll + def handle(%{data: %{"type" => "ClosePoll"}} = object, meta) do + Notification.create_notifications(object) + + {:ok, object, meta} + end + + # Tasks this handles: # - Delete and unpins the create activity # - Replace object with Tombstone # - Set up notification diff --git a/lib/pleroma/web/common_api.ex b/lib/pleroma/web/common_api.ex index e59254791..4b5dc1261 100644 --- a/lib/pleroma/web/common_api.ex +++ b/lib/pleroma/web/common_api.ex @@ -264,6 +264,18 @@ defmodule Pleroma.Web.CommonAPI do end end + # postprocess the close poll + # + def close_poll(activity_id) do + with %Activity{} = activity <- Activity.get_by_id(activity_id), + %Object{} = object <- Object.normalize(activity), + {:ok, poll, _} <- Builder.close_poll(activity, object), + meta <- [local: true, do_not_federate: true], + {:ok, activity, _} <- Pipeline.common_pipeline(poll, meta) do + {:ok, activity} + end + end + def react_with_emoji(id, user, emoji) do with %Activity{} = activity <- Activity.get_by_id(id), object <- Object.normalize(activity), @@ -411,6 +423,25 @@ defmodule Pleroma.Web.CommonAPI do end end + def postprocess(%Activity{} = activity) do + case Object.normalize(activity) do + %Object{data: %{"type" => "Question", "closed" => closed_at}} -> + Pleroma.Workers.PollExpirationNotify.enqueue(%{ + activity_id: activity.id, + closed_at: + Timex.shift( + Timex.parse!(closed_at, "{ISO:Extended:Z}"), + minutes: 1 + ) + }) + + _ -> + activity + end + + activity + end + def pin(id, %{ap_id: user_ap_id} = user) do with %Activity{ actor: ^user_ap_id, diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 9e3a584f0..374f18602 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -164,7 +164,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, _) do params = Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id]) - with {:ok, activity} <- CommonAPI.post(user, params) do + with {:ok, activity} <- CommonAPI.post(user, params), + _ <- CommonAPI.postprocess(activity) do try_render(conn, "show.json", activity: activity, for: user, diff --git a/lib/pleroma/workers/poll_expiration_notify.ex b/lib/pleroma/workers/poll_expiration_notify.ex new file mode 100644 index 000000000..2adf6e940 --- /dev/null +++ b/lib/pleroma/workers/poll_expiration_notify.ex @@ -0,0 +1,25 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.PollExpirationNotify do + @moduledoc false + + use Oban.Worker, queue: :poll_expiration_notify, max_attempts: 1 + + def enqueue(args) do + {scheduled_at, args} = Map.pop(args, :closed_at) + + args + |> __MODULE__.new(scheduled_at: scheduled_at) + |> Oban.insert() + end + + @impl true + def perform(%Oban.Job{args: %{"activity_id" => activity_id}}) do + Pleroma.Web.CommonAPI.close_poll(activity_id) + :ok + end + + def perform(_), do: :ok +end