Add support for activity expiration to common and Masto API

The "expires_at" parameter accepts an ISO8601-formatted date which
defines when the activity will expire.

At this point the API will not give you any feedback about if your post
will expire or not.
This commit is contained in:
Mike Verdone 2019-07-22 16:46:20 +02:00
parent 378f5f0fbe
commit 704960b3c1
6 changed files with 82 additions and 13 deletions

View File

@ -79,6 +79,7 @@ Additional parameters can be added to the JSON body/Form data:
- `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint. - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.
- `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply. - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply.
- `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`. - `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`.
- `expires_on`: datetime (iso8601), sets when the posted activity should expire. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated.
## PATCH `/api/v1/update_credentials` ## PATCH `/api/v1/update_credentials`

View File

@ -10,6 +10,7 @@ defmodule Pleroma.ActivityExpiration do
alias Pleroma.FlakeId alias Pleroma.FlakeId
alias Pleroma.Repo alias Pleroma.Repo
import Ecto.Changeset
import Ecto.Query import Ecto.Query
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@ -19,6 +20,24 @@ defmodule Pleroma.ActivityExpiration do
field(:scheduled_at, :naive_datetime) field(:scheduled_at, :naive_datetime)
end end
def changeset(%ActivityExpiration{} = expiration, attrs) do
expiration
|> cast(attrs, [:scheduled_at])
|> validate_required([:scheduled_at])
end
def get_by_activity_id(activity_id) do
ActivityExpiration
|> where([exp], exp.activity_id == ^activity_id)
|> Repo.one()
end
def create(%Activity{} = activity, scheduled_at) do
%ActivityExpiration{activity_id: activity.id}
|> changeset(%{scheduled_at: scheduled_at})
|> Repo.insert()
end
def due_expirations(offset \\ 0) do def due_expirations(offset \\ 0) do
naive_datetime = naive_datetime =
NaiveDateTime.utc_now() NaiveDateTime.utc_now()

View File

@ -4,6 +4,7 @@
defmodule Pleroma.Web.CommonAPI do defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
@ -218,6 +219,7 @@ def post(user, %{"status" => status} = data) do
context <- make_context(in_reply_to), context <- make_context(in_reply_to),
cw <- data["spoiler_text"] || "", cw <- data["spoiler_text"] || "",
sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
{:ok, expires_at} <- Ecto.Type.cast(:naive_datetime, data["expires_at"]),
full_payload <- String.trim(status <> cw), full_payload <- String.trim(status <> cw),
:ok <- validate_character_limit(full_payload, attachments, limit), :ok <- validate_character_limit(full_payload, attachments, limit),
object <- object <-
@ -243,15 +245,24 @@ def post(user, %{"status" => status} = data) do
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
direct? = visibility == "direct" direct? = visibility == "direct"
%{ result =
to: to, %{
actor: user, to: to,
context: context, actor: user,
object: object, context: context,
additional: %{"cc" => cc, "directMessage" => direct?} object: object,
} additional: %{"cc" => cc, "directMessage" => direct?}
|> maybe_add_list_data(user, visibility) }
|> ActivityPub.create(preview?) |> maybe_add_list_data(user, visibility)
|> ActivityPub.create(preview?)
if expires_at do
with {:ok, activity} <- result do
ActivityExpiration.create(activity, expires_at)
end
end
result
else else
{:private_to_public, true} -> {:private_to_public, true} ->
{:error, dgettext("errors", "The message visibility must be direct")} {:error, dgettext("errors", "The message visibility must be direct")}

View File

@ -143,12 +143,14 @@ def note_activity_factory(attrs \\ %{}) do
end end
defp expiration_offset_by_minutes(attrs, minutes) do defp expiration_offset_by_minutes(attrs, minutes) do
scheduled_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(minutes), :millisecond)
|> NaiveDateTime.truncate(:second)
%Pleroma.ActivityExpiration{} %Pleroma.ActivityExpiration{}
|> Map.merge(attrs) |> Map.merge(attrs)
|> Map.put( |> Map.put(:scheduled_at, scheduled_at)
:scheduled_at,
NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(minutes), :millisecond)
)
end end
def expiration_in_the_past_factory(attrs \\ %{}) do def expiration_in_the_past_factory(attrs \\ %{}) do

View File

@ -160,6 +160,23 @@ test "it returns error when character limit is exceeded" do
Pleroma.Config.put([:instance, :limit], limit) Pleroma.Config.put([:instance, :limit], limit)
end end
test "it can handle activities that expire" do
user = insert(:user)
expires_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.truncate(:second)
|> NaiveDateTime.add(1_000_000, :second)
expires_at_iso8601 = expires_at |> NaiveDateTime.to_iso8601()
assert {:ok, activity} =
CommonAPI.post(user, %{"status" => "chai", "expires_at" => expires_at_iso8601})
assert expiration = Pleroma.ActivityExpiration.get_by_activity_id(activity.id)
assert expiration.scheduled_at == expires_at
end
end end
describe "reactions" do describe "reactions" do

View File

@ -7,6 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -151,6 +152,24 @@ test "posting a status", %{conn: conn} do
assert %{"id" => third_id} = json_response(conn_three, 200) assert %{"id" => third_id} = json_response(conn_three, 200)
refute id == third_id refute id == third_id
# An activity that will expire:
expires_at =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(:timer.minutes(120), :millisecond)
|> NaiveDateTime.truncate(:second)
conn_four =
conn
|> post("api/v1/statuses", %{
"status" => "oolong",
"expires_at" => expires_at
})
assert %{"id" => fourth_id} = json_response(conn_four, 200)
assert activity = Activity.get_by_id(fourth_id)
assert expiration = ActivityExpiration.get_by_activity_id(fourth_id)
assert expiration.scheduled_at == expires_at
end end
test "replying to a status", %{conn: conn} do test "replying to a status", %{conn: conn} do