Added limits and media attachments for scheduled activities.
This commit is contained in:
parent
b3870df51f
commit
fc92a0fd8d
|
@ -367,6 +367,10 @@
|
|||
enabled: false,
|
||||
pages: 5
|
||||
|
||||
config :pleroma, Pleroma.ScheduledActivity,
|
||||
daily_user_limit: 25,
|
||||
total_user_limit: 100
|
||||
|
||||
config :auto_linker,
|
||||
opts: [
|
||||
scheme: true,
|
||||
|
|
|
@ -218,14 +218,14 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
|
|||
- `port`
|
||||
* `url` - a list containing the configuration for generating urls, accepts
|
||||
- `host` - the host without the scheme and a post (e.g `example.com`, not `https://example.com:2020`)
|
||||
- `scheme` - e.g `http`, `https`
|
||||
- `scheme` - e.g `http`, `https`
|
||||
- `port`
|
||||
- `path`
|
||||
|
||||
|
||||
**Important note**: if you modify anything inside these lists, default `config.exs` values will be overwritten, which may result in breakage, to make sure this does not happen please copy the default value for the list from `config.exs` and modify/add only what you need
|
||||
|
||||
Example:
|
||||
Example:
|
||||
```elixir
|
||||
config :pleroma, Pleroma.Web.Endpoint,
|
||||
url: [host: "example.com", port: 2020, scheme: "https"],
|
||||
|
@ -412,3 +412,8 @@ Pleroma account will be created with the same name as the LDAP user name.
|
|||
|
||||
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator
|
||||
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication
|
||||
|
||||
## Pleroma.ScheduledActivity
|
||||
|
||||
* `daily_user_limit`: the number of scheduled activities a user is allowed to create in a single day
|
||||
* `total_user_limit`: the number of scheduled activities a user is allowed to create in total
|
||||
|
|
|
@ -184,4 +184,12 @@ def decrease_replies_count(ap_id) do
|
|||
_ -> {:error, "Not found"}
|
||||
end
|
||||
end
|
||||
|
||||
def enforce_user_objects(user, object_ids) do
|
||||
Object
|
||||
|> where([o], fragment("?->>'actor' = ?", o.data, ^user.ap_id))
|
||||
|> where([o], o.id in ^object_ids)
|
||||
|> select([o], o.id)
|
||||
|> Repo.all()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,9 +5,12 @@
|
|||
defmodule Pleroma.ScheduledActivity do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.ScheduledActivity
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
|
@ -25,11 +28,69 @@ defmodule Pleroma.ScheduledActivity do
|
|||
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
|
||||
user = User.get_cached_by_id(changeset.data.user_id)
|
||||
media_ids = Object.enforce_user_objects(user, media_ids) |> Enum.map(&to_string(&1))
|
||||
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([s], type(s.scheduled_at, :date) == type(^scheduled_at, :date))
|
||||
|> select([u], count(u.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([u], count(u.id))
|
||||
|> Repo.one()
|
||||
|> Kernel.>=(Config.get([ScheduledActivity, :total_user_limit]))
|
||||
end
|
||||
|
||||
def far_enough?(scheduled_at) when is_binary(scheduled_at) do
|
||||
|
@ -64,23 +125,15 @@ def get(%User{} = user, scheduled_activity_id) do
|
|||
|> Repo.one()
|
||||
end
|
||||
|
||||
def update(%User{} = user, scheduled_activity_id, attrs) do
|
||||
with %ScheduledActivity{} = scheduled_activity <- get(user, scheduled_activity_id) do
|
||||
scheduled_activity
|
||||
|> update_changeset(attrs)
|
||||
|> Repo.update()
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
def update(scheduled_activity, attrs) do
|
||||
scheduled_activity
|
||||
|> update_changeset(attrs)
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def delete(%User{} = user, scheduled_activity_id) do
|
||||
with %ScheduledActivity{} = scheduled_activity <- get(user, scheduled_activity_id) do
|
||||
scheduled_activity
|
||||
|> Repo.delete()
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
end
|
||||
def delete(scheduled_activity) do
|
||||
scheduled_activity
|
||||
|> Repo.delete()
|
||||
end
|
||||
|
||||
def for_user_query(%User{} = user) do
|
||||
|
|
|
@ -390,18 +390,28 @@ def update_scheduled_status(
|
|||
%{assigns: %{user: user}} = conn,
|
||||
%{"id" => scheduled_activity_id} = params
|
||||
) do
|
||||
with {:ok, scheduled_activity} <-
|
||||
ScheduledActivity.update(user, 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 {:ok, %ScheduledActivity{}} <- ScheduledActivity.delete(user, scheduled_activity_id) do
|
||||
with %ScheduledActivity{} = scheduled_activity <-
|
||||
ScheduledActivity.get(user, scheduled_activity_id),
|
||||
{:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
|
||||
conn
|
||||
|> json(%{})
|
||||
|> put_view(ScheduledActivityView)
|
||||
|> render("show.json", %{scheduled_activity: scheduled_activity})
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
|
|||
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")
|
||||
|
@ -17,7 +18,36 @@ def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_a
|
|||
%{
|
||||
id: scheduled_activity.id |> to_string,
|
||||
scheduled_at: scheduled_activity.scheduled_at |> CommonAPI.Utils.to_masto_date(),
|
||||
params: scheduled_activity.params
|
||||
params: status_params(scheduled_activity.params)
|
||||
}
|
||||
|> with_media_attachments(scheduled_activity)
|
||||
end
|
||||
|
||||
defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do
|
||||
attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
|
||||
Map.put(data, :media_attachments, attachments)
|
||||
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
|
||||
|
|
|
@ -11,5 +11,6 @@ def change do
|
|||
end
|
||||
|
||||
create(index(:scheduled_activities, [:scheduled_at]))
|
||||
create(index(:scheduled_activities, [:user_id]))
|
||||
end
|
||||
end
|
||||
|
|
93
test/scheduled_activity_test.exs
Normal file
93
test/scheduled_activity_test.exs
Normal file
|
@ -0,0 +1,93 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.ScheduledActivityTest do
|
||||
use Pleroma.DataCase
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.DataCase
|
||||
alias Pleroma.ScheduledActivity
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
import Pleroma.Factory
|
||||
|
||||
setup context do
|
||||
Config.put([ScheduledActivity, :daily_user_limit], 2)
|
||||
Config.put([ScheduledActivity, :total_user_limit], 3)
|
||||
DataCase.ensure_local_uploader(context)
|
||||
end
|
||||
|
||||
describe "creation" do
|
||||
test "when daily user limit is exceeded" do
|
||||
user = insert(:user)
|
||||
|
||||
today =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|
||||
|> NaiveDateTime.to_iso8601()
|
||||
|
||||
attrs = %{params: %{}, scheduled_at: today}
|
||||
{:ok, _} = ScheduledActivity.create(user, attrs)
|
||||
{:ok, _} = ScheduledActivity.create(user, attrs)
|
||||
{:error, changeset} = ScheduledActivity.create(user, attrs)
|
||||
assert changeset.errors == [scheduled_at: {"daily limit exceeded", []}]
|
||||
end
|
||||
|
||||
test "when total user limit is exceeded" do
|
||||
user = insert(:user)
|
||||
|
||||
today =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(:timer.minutes(6), :millisecond)
|
||||
|> NaiveDateTime.to_iso8601()
|
||||
|
||||
tomorrow =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(:timer.hours(24), :millisecond)
|
||||
|> NaiveDateTime.to_iso8601()
|
||||
|
||||
{:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today})
|
||||
{:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: today})
|
||||
{:ok, _} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
|
||||
{:error, changeset} = ScheduledActivity.create(user, %{params: %{}, scheduled_at: tomorrow})
|
||||
assert changeset.errors == [scheduled_at: {"total limit exceeded", []}]
|
||||
end
|
||||
|
||||
test "when scheduled_at is earlier than 5 minute from now" do
|
||||
user = insert(:user)
|
||||
|
||||
scheduled_at =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(:timer.minutes(4), :millisecond)
|
||||
|> NaiveDateTime.to_iso8601()
|
||||
|
||||
attrs = %{params: %{}, scheduled_at: scheduled_at}
|
||||
{:error, changeset} = ScheduledActivity.create(user, attrs)
|
||||
assert changeset.errors == [scheduled_at: {"must be at least 5 minutes from now", []}]
|
||||
end
|
||||
|
||||
test "excludes attachments belonging to another user" do
|
||||
user = insert(:user)
|
||||
another_user = insert(:user)
|
||||
|
||||
scheduled_at =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(:timer.minutes(10), :millisecond)
|
||||
|> NaiveDateTime.to_iso8601()
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, user_upload} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
{:ok, another_user_upload} = ActivityPub.upload(file, actor: another_user.ap_id)
|
||||
|
||||
media_ids = [user_upload.id, another_user_upload.id]
|
||||
attrs = %{params: %{"media_ids" => media_ids}, scheduled_at: scheduled_at}
|
||||
{:ok, scheduled_activity} = ScheduledActivity.create(user, attrs)
|
||||
assert to_string(user_upload.id) in scheduled_activity.params["media_ids"]
|
||||
refute to_string(another_user_upload.id) in scheduled_activity.params["media_ids"]
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,14 +23,6 @@ def user_factory do
|
|||
}
|
||||
end
|
||||
|
||||
def scheduled_activity_factory do
|
||||
%Pleroma.ScheduledActivity{
|
||||
user: build(:user),
|
||||
scheduled_at: NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(60), :millisecond),
|
||||
params: build(:note) |> Map.from_struct() |> Map.get(:data)
|
||||
}
|
||||
end
|
||||
|
||||
def note_factory(attrs \\ %{}) do
|
||||
text = sequence(:text, &"This is :moominmamma: note #{&1}")
|
||||
|
||||
|
@ -275,4 +267,12 @@ def notification_factory do
|
|||
user: build(:user)
|
||||
}
|
||||
end
|
||||
|
||||
def scheduled_activity_factory do
|
||||
%Pleroma.ScheduledActivity{
|
||||
user: build(:user),
|
||||
scheduled_at: NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(60), :millisecond),
|
||||
params: build(:note) |> Map.from_struct() |> Map.get(:data)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2427,6 +2427,31 @@ test "creates a scheduled activity", %{conn: conn} do
|
|||
assert [] == Repo.all(Activity)
|
||||
end
|
||||
|
||||
test "creates a scheduled activity with a media attachment", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|> post("/api/v1/statuses", %{
|
||||
"media_ids" => [to_string(upload.id)],
|
||||
"status" => "scheduled",
|
||||
"scheduled_at" => scheduled_at
|
||||
})
|
||||
|
||||
assert %{"media_attachments" => [media_attachment]} = json_response(conn, 200)
|
||||
assert %{"type" => "image"} = media_attachment
|
||||
end
|
||||
|
||||
test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
|
||||
%{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
|
68
test/web/mastodon_api/scheduled_activity_view_test.exs
Normal file
68
test/web/mastodon_api/scheduled_activity_view_test.exs
Normal file
|
@ -0,0 +1,68 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do
|
||||
use Pleroma.DataCase
|
||||
alias Pleroma.ScheduledActivity
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
import Pleroma.Factory
|
||||
|
||||
test "A scheduled activity with a media attachment" do
|
||||
user = insert(:user)
|
||||
{:ok, activity} = CommonAPI.post(user, %{"status" => "hi"})
|
||||
|
||||
scheduled_at =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.add(:timer.minutes(10), :millisecond)
|
||||
|> NaiveDateTime.to_iso8601()
|
||||
|
||||
file = %Plug.Upload{
|
||||
content_type: "image/jpg",
|
||||
path: Path.absname("test/fixtures/image.jpg"),
|
||||
filename: "an_image.jpg"
|
||||
}
|
||||
|
||||
{:ok, upload} = ActivityPub.upload(file, actor: user.ap_id)
|
||||
|
||||
attrs = %{
|
||||
params: %{
|
||||
"media_ids" => [upload.id],
|
||||
"status" => "hi",
|
||||
"sensitive" => true,
|
||||
"spoiler_text" => "spoiler",
|
||||
"visibility" => "unlisted",
|
||||
"in_reply_to_id" => to_string(activity.id)
|
||||
},
|
||||
scheduled_at: scheduled_at
|
||||
}
|
||||
|
||||
{:ok, scheduled_activity} = ScheduledActivity.create(user, attrs)
|
||||
result = ScheduledActivityView.render("show.json", %{scheduled_activity: scheduled_activity})
|
||||
|
||||
expected = %{
|
||||
id: to_string(scheduled_activity.id),
|
||||
media_attachments:
|
||||
%{"media_ids" => [upload.id]}
|
||||
|> Utils.attachments_from_ids()
|
||||
|> Enum.map(&StatusView.render("attachment.json", %{attachment: &1})),
|
||||
params: %{
|
||||
in_reply_to_id: to_string(activity.id),
|
||||
media_ids: [to_string(upload.id)],
|
||||
poll: nil,
|
||||
scheduled_at: nil,
|
||||
sensitive: true,
|
||||
spoiler_text: "spoiler",
|
||||
text: "hi",
|
||||
visibility: "unlisted"
|
||||
},
|
||||
scheduled_at: Utils.to_masto_date(scheduled_activity.scheduled_at)
|
||||
}
|
||||
|
||||
assert expected == result
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue