aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/pleroma/activity.ex2
-rw-r--r--lib/pleroma/application.ex3
-rw-r--r--lib/pleroma/scheduled_activity.ex161
-rw-r--r--lib/pleroma/scheduled_activity_worker.ex58
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api.ex7
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api_controller.ex94
-rw-r--r--lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex57
-rw-r--r--lib/pleroma/web/router.ex6
8 files changed, 381 insertions, 7 deletions
diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex
index bc3f8caba..ab8861b27 100644
--- a/lib/pleroma/activity.ex
+++ b/lib/pleroma/activity.ex
@@ -31,7 +31,7 @@ defmodule Pleroma.Activity do
field(:data, :map)
field(:local, :boolean, default: true)
field(:actor, :string)
- field(:recipients, {:array, :string})
+ field(:recipients, {:array, :string}, default: [])
has_many(:notifications, Notification, on_delete: :delete_all)
# Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 1fc3fb728..f0cb7d9a8 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -104,7 +104,8 @@ defmodule Pleroma.Application do
],
id: :cachex_idem
),
- worker(Pleroma.FlakeId, [])
+ worker(Pleroma.FlakeId, []),
+ worker(Pleroma.ScheduledActivityWorker, [])
] ++
hackney_pool_children() ++
[
diff --git a/lib/pleroma/scheduled_activity.ex b/lib/pleroma/scheduled_activity.ex
new file mode 100644
index 000000000..de0e54699
--- /dev/null
+++ b/lib/pleroma/scheduled_activity.ex
@@ -0,0 +1,161 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ScheduledActivity do
+ use Ecto.Schema
+
+ alias Pleroma.Config
+ alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI.Utils
+
+ import Ecto.Query
+ import Ecto.Changeset
+
+ @min_offset :timer.minutes(5)
+
+ schema "scheduled_activities" do
+ belongs_to(:user, User, type: Pleroma.FlakeId)
+ field(:scheduled_at, :naive_datetime)
+ field(:params, :map)
+
+ timestamps()
+ end
+
+ def changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
+ scheduled_activity
+ |> cast(attrs, [:scheduled_at, :params])
+ |> validate_required([:scheduled_at, :params])
+ |> validate_scheduled_at()
+ |> with_media_attachments()
+ end
+
+ defp with_media_attachments(
+ %{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
+ )
+ when is_list(media_ids) do
+ media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids})
+
+ params =
+ params
+ |> Map.put("media_attachments", media_attachments)
+ |> Map.put("media_ids", media_ids)
+
+ put_change(changeset, :params, params)
+ end
+
+ defp with_media_attachments(changeset), do: changeset
+
+ def update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
+ scheduled_activity
+ |> cast(attrs, [:scheduled_at])
+ |> validate_required([:scheduled_at])
+ |> validate_scheduled_at()
+ end
+
+ def validate_scheduled_at(changeset) do
+ validate_change(changeset, :scheduled_at, fn _, scheduled_at ->
+ cond do
+ not far_enough?(scheduled_at) ->
+ [scheduled_at: "must be at least 5 minutes from now"]
+
+ exceeds_daily_user_limit?(changeset.data.user_id, scheduled_at) ->
+ [scheduled_at: "daily limit exceeded"]
+
+ exceeds_total_user_limit?(changeset.data.user_id) ->
+ [scheduled_at: "total limit exceeded"]
+
+ true ->
+ []
+ end
+ end)
+ end
+
+ def exceeds_daily_user_limit?(user_id, scheduled_at) do
+ ScheduledActivity
+ |> where(user_id: ^user_id)
+ |> where([sa], type(sa.scheduled_at, :date) == type(^scheduled_at, :date))
+ |> select([sa], count(sa.id))
+ |> Repo.one()
+ |> Kernel.>=(Config.get([ScheduledActivity, :daily_user_limit]))
+ end
+
+ def exceeds_total_user_limit?(user_id) do
+ ScheduledActivity
+ |> where(user_id: ^user_id)
+ |> select([sa], count(sa.id))
+ |> Repo.one()
+ |> Kernel.>=(Config.get([ScheduledActivity, :total_user_limit]))
+ end
+
+ def far_enough?(scheduled_at) when is_binary(scheduled_at) do
+ with {:ok, scheduled_at} <- Ecto.Type.cast(:naive_datetime, scheduled_at) do
+ far_enough?(scheduled_at)
+ else
+ _ -> false
+ end
+ end
+
+ def far_enough?(scheduled_at) do
+ now = NaiveDateTime.utc_now()
+ diff = NaiveDateTime.diff(scheduled_at, now, :millisecond)
+ diff > @min_offset
+ end
+
+ def new(%User{} = user, attrs) do
+ %ScheduledActivity{user_id: user.id}
+ |> changeset(attrs)
+ end
+
+ def create(%User{} = user, attrs) do
+ user
+ |> new(attrs)
+ |> Repo.insert()
+ end
+
+ def get(%User{} = user, scheduled_activity_id) do
+ ScheduledActivity
+ |> where(user_id: ^user.id)
+ |> where(id: ^scheduled_activity_id)
+ |> Repo.one()
+ end
+
+ def update(%ScheduledActivity{} = scheduled_activity, attrs) do
+ scheduled_activity
+ |> update_changeset(attrs)
+ |> Repo.update()
+ end
+
+ def delete(%ScheduledActivity{} = scheduled_activity) do
+ scheduled_activity
+ |> Repo.delete()
+ end
+
+ def delete(id) when is_binary(id) or is_integer(id) do
+ ScheduledActivity
+ |> where(id: ^id)
+ |> select([sa], sa)
+ |> Repo.delete_all()
+ |> case do
+ {1, [scheduled_activity]} -> {:ok, scheduled_activity}
+ _ -> :error
+ end
+ end
+
+ def for_user_query(%User{} = user) do
+ ScheduledActivity
+ |> where(user_id: ^user.id)
+ end
+
+ def due_activities(offset \\ 0) do
+ naive_datetime =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.add(offset, :millisecond)
+
+ ScheduledActivity
+ |> where([sa], sa.scheduled_at < ^naive_datetime)
+ |> Repo.all()
+ end
+end
diff --git a/lib/pleroma/scheduled_activity_worker.ex b/lib/pleroma/scheduled_activity_worker.ex
new file mode 100644
index 000000000..65b38622f
--- /dev/null
+++ b/lib/pleroma/scheduled_activity_worker.ex
@@ -0,0 +1,58 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.ScheduledActivityWorker do
+ @moduledoc """
+ Sends scheduled activities to the job queue.
+ """
+
+ alias Pleroma.Config
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+ use GenServer
+ require Logger
+
+ @schedule_interval :timer.minutes(1)
+
+ def start_link do
+ GenServer.start_link(__MODULE__, nil)
+ end
+
+ def init(_) do
+ if Config.get([ScheduledActivity, :enabled]) do
+ schedule_next()
+ {:ok, nil}
+ else
+ :ignore
+ end
+ end
+
+ def perform(:execute, scheduled_activity_id) do
+ try do
+ {:ok, scheduled_activity} = ScheduledActivity.delete(scheduled_activity_id)
+ %User{} = user = User.get_cached_by_id(scheduled_activity.user_id)
+ {:ok, _result} = CommonAPI.post(user, scheduled_activity.params)
+ rescue
+ error ->
+ Logger.error(
+ "#{__MODULE__} Couldn't create a status from the scheduled activity: #{inspect(error)}"
+ )
+ end
+ end
+
+ def handle_info(:perform, state) do
+ ScheduledActivity.due_activities(@schedule_interval)
+ |> Enum.each(fn scheduled_activity ->
+ PleromaJobQueue.enqueue(:scheduled_activities, __MODULE__, [:execute, scheduled_activity.id])
+ end)
+
+ schedule_next()
+ {:noreply, state}
+ end
+
+ defp schedule_next do
+ Process.send_after(self(), :perform, @schedule_interval)
+ end
+end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex
index 08ea5f967..382f07e6b 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex
@@ -5,6 +5,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Pagination
+ alias Pleroma.ScheduledActivity
alias Pleroma.User
def get_followers(user, params \\ %{}) do
@@ -28,6 +29,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|> Pagination.fetch_paginated(params)
end
+ def get_scheduled_activities(user, params \\ %{}) do
+ user
+ |> ScheduledActivity.for_user_query()
+ |> Pagination.fetch_paginated(params)
+ end
+
defp cast_params(params) do
param_types = %{
exclude_types: {:array, :string}
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index bcc79b08a..fc8a2458c 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -5,12 +5,14 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
+ alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Filter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.ScheduledActivity
alias Pleroma.Stats
alias Pleroma.User
alias Pleroma.Web
@@ -25,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.ReportView
+ alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OAuth.App
@@ -364,6 +367,55 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
end
end
+ def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
+ with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
+ conn
+ |> add_link_headers(:scheduled_statuses, scheduled_activities)
+ |> put_view(ScheduledActivityView)
+ |> render("index.json", %{scheduled_activities: scheduled_activities})
+ end
+ end
+
+ def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
+ with %ScheduledActivity{} = scheduled_activity <-
+ ScheduledActivity.get(user, scheduled_activity_id) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ else
+ _ -> {:error, :not_found}
+ end
+ end
+
+ def update_scheduled_status(
+ %{assigns: %{user: user}} = conn,
+ %{"id" => scheduled_activity_id} = params
+ ) do
+ with %ScheduledActivity{} = scheduled_activity <-
+ ScheduledActivity.get(user, scheduled_activity_id),
+ {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ else
+ nil -> {:error, :not_found}
+ error -> error
+ end
+ end
+
+ def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
+ with %ScheduledActivity{} = scheduled_activity <-
+ ScheduledActivity.get(user, scheduled_activity_id),
+ {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ else
+ nil -> {:error, :not_found}
+ error -> error
+ end
+ end
+
def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
when length(media_ids) > 0 do
params =
@@ -384,12 +436,27 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
_ -> Ecto.UUID.generate()
end
- {:ok, activity} =
- Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
+ scheduled_at = params["scheduled_at"]
- conn
- |> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
+ with {:ok, scheduled_activity} <-
+ ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
+ conn
+ |> put_view(ScheduledActivityView)
+ |> render("show.json", %{scheduled_activity: scheduled_activity})
+ end
+ else
+ params = Map.drop(params, ["scheduled_at"])
+
+ {:ok, activity} =
+ Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
+ CommonAPI.post(user, params)
+ end)
+
+ conn
+ |> put_view(StatusView)
+ |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ end
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
@@ -1406,6 +1473,23 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
# fallback action
#
+ def errors(conn, {:error, %Changeset{} = changeset}) do
+ error_message =
+ changeset
+ |> Changeset.traverse_errors(fn {message, _opt} -> message end)
+ |> Enum.map_join(", ", fn {_k, v} -> v end)
+
+ conn
+ |> put_status(422)
+ |> json(%{error: error_message})
+ end
+
+ def errors(conn, {:error, :not_found}) do
+ conn
+ |> put_status(404)
+ |> json(%{error: "Record not found"})
+ end
+
def errors(conn, _) do
conn
|> put_status(500)
diff --git a/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
new file mode 100644
index 000000000..0aae15ab9
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
@@ -0,0 +1,57 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
+ use Pleroma.Web, :view
+
+ alias Pleroma.ScheduledActivity
+ alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.MastodonAPI.ScheduledActivityView
+ alias Pleroma.Web.MastodonAPI.StatusView
+
+ def render("index.json", %{scheduled_activities: scheduled_activities}) do
+ render_many(scheduled_activities, ScheduledActivityView, "show.json")
+ end
+
+ def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do
+ %{
+ id: to_string(scheduled_activity.id),
+ scheduled_at: CommonAPI.Utils.to_masto_date(scheduled_activity.scheduled_at),
+ params: status_params(scheduled_activity.params)
+ }
+ |> with_media_attachments(scheduled_activity)
+ end
+
+ defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do
+ try do
+ attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
+ Map.put(data, :media_attachments, attachments)
+ rescue
+ _ -> data
+ end
+ end
+
+ defp with_media_attachments(data, _), do: data
+
+ defp status_params(params) do
+ data = %{
+ text: params["status"],
+ sensitive: params["sensitive"],
+ spoiler_text: params["spoiler_text"],
+ visibility: params["visibility"],
+ scheduled_at: params["scheduled_at"],
+ poll: params["poll"],
+ in_reply_to_id: params["in_reply_to_id"]
+ }
+
+ data =
+ if media_ids = params["media_ids"] do
+ Map.put(data, :media_ids, media_ids)
+ else
+ data
+ end
+
+ data
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 1c752e44c..3b5ac6fdd 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -244,6 +244,9 @@ defmodule Pleroma.Web.Router do
get("/notifications", MastodonAPIController, :notifications)
get("/notifications/:id", MastodonAPIController, :get_notification)
+ get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
+ get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
+
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
@@ -278,6 +281,9 @@ defmodule Pleroma.Web.Router do
post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
+ put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
+ delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
+
post("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media)