Added limits and media attachments for scheduled activities.

This commit is contained in:
eugenijm 2019-04-02 01:31:01 +03:00
parent b3870df51f
commit fc92a0fd8d
11 changed files with 327 additions and 30 deletions

View File

@ -367,6 +367,10 @@
enabled: false, enabled: false,
pages: 5 pages: 5
config :pleroma, Pleroma.ScheduledActivity,
daily_user_limit: 25,
total_user_limit: 100
config :auto_linker, config :auto_linker,
opts: [ opts: [
scheme: true, scheme: true,

View File

@ -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.PleromaAuthenticator`: default database authenticator
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication * `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

View File

@ -184,4 +184,12 @@ def decrease_replies_count(ap_id) do
_ -> {:error, "Not found"} _ -> {:error, "Not found"}
end end
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 end

View File

@ -5,9 +5,12 @@
defmodule Pleroma.ScheduledActivity do defmodule Pleroma.ScheduledActivity do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Config
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ScheduledActivity alias Pleroma.ScheduledActivity
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
import Ecto.Query import Ecto.Query
import Ecto.Changeset import Ecto.Changeset
@ -25,11 +28,69 @@ defmodule Pleroma.ScheduledActivity do
def changeset(%ScheduledActivity{} = scheduled_activity, attrs) do def changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
scheduled_activity scheduled_activity
|> cast(attrs, [:scheduled_at, :params]) |> cast(attrs, [:scheduled_at, :params])
|> validate_required([:scheduled_at, :params])
|> validate_scheduled_at()
|> with_media_attachments()
end 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 def update_changeset(%ScheduledActivity{} = scheduled_activity, attrs) do
scheduled_activity scheduled_activity
|> cast(attrs, [:scheduled_at]) |> 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 end
def far_enough?(scheduled_at) when is_binary(scheduled_at) do 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() |> Repo.one()
end end
def update(%User{} = user, scheduled_activity_id, attrs) do def update(scheduled_activity, attrs) do
with %ScheduledActivity{} = scheduled_activity <- get(user, scheduled_activity_id) do scheduled_activity
scheduled_activity |> update_changeset(attrs)
|> update_changeset(attrs) |> Repo.update()
|> Repo.update()
else
nil -> {:error, :not_found}
end
end end
def delete(%User{} = user, scheduled_activity_id) do def delete(scheduled_activity) do
with %ScheduledActivity{} = scheduled_activity <- get(user, scheduled_activity_id) do scheduled_activity
scheduled_activity |> Repo.delete()
|> Repo.delete()
else
nil -> {:error, :not_found}
end
end end
def for_user_query(%User{} = user) do def for_user_query(%User{} = user) do

View File

@ -390,18 +390,28 @@ def update_scheduled_status(
%{assigns: %{user: user}} = conn, %{assigns: %{user: user}} = conn,
%{"id" => scheduled_activity_id} = params %{"id" => scheduled_activity_id} = params
) do ) do
with {:ok, scheduled_activity} <- with %ScheduledActivity{} = scheduled_activity <-
ScheduledActivity.update(user, scheduled_activity_id, params) do ScheduledActivity.get(user, scheduled_activity_id),
{:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
conn conn
|> put_view(ScheduledActivityView) |> put_view(ScheduledActivityView)
|> render("show.json", %{scheduled_activity: scheduled_activity}) |> render("show.json", %{scheduled_activity: scheduled_activity})
else
nil -> {:error, :not_found}
error -> error
end end
end end
def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do 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 conn
|> json(%{}) |> put_view(ScheduledActivityView)
|> render("show.json", %{scheduled_activity: scheduled_activity})
else
nil -> {:error, :not_found}
error -> error
end end
end end

View File

@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
alias Pleroma.ScheduledActivity alias Pleroma.ScheduledActivity
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ScheduledActivityView alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{scheduled_activities: scheduled_activities}) do def render("index.json", %{scheduled_activities: scheduled_activities}) do
render_many(scheduled_activities, ScheduledActivityView, "show.json") 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, id: scheduled_activity.id |> to_string,
scheduled_at: scheduled_activity.scheduled_at |> CommonAPI.Utils.to_masto_date(), 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
end end

View File

@ -11,5 +11,6 @@ def change do
end end
create(index(:scheduled_activities, [:scheduled_at])) create(index(:scheduled_activities, [:scheduled_at]))
create(index(:scheduled_activities, [:user_id]))
end end
end end

View 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

View File

@ -23,14 +23,6 @@ def user_factory do
} }
end 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 def note_factory(attrs \\ %{}) do
text = sequence(:text, &"This is :moominmamma: note #{&1}") text = sequence(:text, &"This is :moominmamma: note #{&1}")
@ -275,4 +267,12 @@ def notification_factory do
user: build(:user) user: build(:user)
} }
end 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 end

View File

@ -2427,6 +2427,31 @@ test "creates a scheduled activity", %{conn: conn} do
assert [] == Repo.all(Activity) assert [] == Repo.all(Activity)
end 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", test "skips the scheduling and creates the activity if scheduled_at is earlier than 5 minutes from now",
%{conn: conn} do %{conn: conn} do
user = insert(:user) user = insert(:user)

View 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