From 0cfd5b4e89b02688342345755577e58eece3db0f Mon Sep 17 00:00:00 2001 From: floatingghost Date: Mon, 28 Nov 2022 13:34:54 +0000 Subject: [PATCH] Add ability to set a default post expiry (#321) Co-authored-by: FloatingGhost Reviewed-on: https://akkoma.dev/AkkomaGang/akkoma/pulls/321 --- CHANGELOG.md | 1 + lib/pleroma/user.ex | 5 +++- .../api_spec/operations/account_operation.ex | 11 +++++-- lib/pleroma/web/api_spec/schemas/account.ex | 6 ++++ .../controllers/account_controller.ex | 7 +++++ .../controllers/status_controller.ex | 10 +++++++ .../web/mastodon_api/views/account_view.ex | 3 +- .../web/mastodon_api/views/status_view.ex | 2 +- ...0220905011454_generate_unset_user_keys.exs | 6 ++-- ...0221128103145_add_per_user_post_expiry.exs | 9 ++++++ .../controllers/status_controller_test.exs | 29 +++++++++++++++++++ .../mastodon_api/update_credentials_test.exs | 20 +++++++++++++ .../mastodon_api/views/account_view_test.exs | 9 ++++-- 13 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 priv/repo/migrations/20221128103145_add_per_user_post_expiry.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index b6eb928c6..b49f77428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Added - Config: HTTP timeout options, :pool\_timeout and :receive\_timeout - Added statistic gathering about instances which do/don't have signed fetches when they request from us +- Ability to set a default post expiry time, after which the post will be deleted. If used in concert with ActivityExpiration MRF, the expiry which comes _sooner_ will be applied. ## Changed - MastoAPI: Accept BooleanLike input on `/api/v1/accounts/:id/follow` (fixes follows with mastodon.py) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index eb907a2d8..b0ab9d0cd 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -151,6 +151,7 @@ defmodule Pleroma.User do field(:is_suggested, :boolean, default: false) field(:last_status_at, :naive_datetime) field(:language, :string) + field(:status_ttl_days, :integer, default: nil) embeds_one( :notification_settings, @@ -516,7 +517,8 @@ def update_changeset(struct, params \\ %{}) do :pleroma_settings_store, :is_discoverable, :actor_type, - :disclose_client + :disclose_client, + :status_ttl_days ] ) |> unique_constraint(:nickname) @@ -524,6 +526,7 @@ def update_changeset(struct, params \\ %{}) do |> validate_length(:bio, max: bio_limit) |> validate_length(:name, min: 1, max: name_limit) |> validate_inclusion(:actor_type, ["Person", "Service"]) + |> validate_number(:status_ttl_days, greater_than: 0) |> put_fields() |> put_emoji() |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)}) diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 359a73ac0..e20f57fec 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -700,7 +700,13 @@ defp update_credentials_request do description: "Discovery (listing, indexing) of this account by external services (search bots etc.) is allowed." }, - actor_type: ActorType + actor_type: ActorType, + status_ttl_days: %Schema{ + type: :integer, + nullable: true, + description: + "Number of days after which statuses will be deleted. Set to -1 to disable." + } }, example: %{ bot: false, @@ -720,7 +726,8 @@ defp update_credentials_request do allow_following_move: false, also_known_as: ["https://foo.bar/users/foo"], discoverable: false, - actor_type: "Person" + actor_type: "Person", + status_ttl_days: 30 } } end diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 5d3ac9cd0..2693eaceb 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -109,6 +109,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do } } }, + akkoma: %Schema{ + type: :object, + properties: %{ + note_ttl_days: %Schema{type: :integer} + } + }, source: %Schema{ type: :object, properties: %{ diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index 946c8544f..80497a252 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -175,6 +175,11 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p value -> {:ok, value} end + status_ttl_days_value = fn + -1 -> {:ok, nil} + value -> {:ok, value} + end + user_params = [ :no_rich_text, @@ -215,7 +220,9 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p # Note: param name is indeed :discoverable (not an error) |> Maps.put_if_present(:is_discoverable, params[:discoverable]) |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language])) + |> Maps.put_if_present(:status_ttl_days, params[:status_ttl_days], status_ttl_days_value) + IO.inspect(user_params) # What happens here: # # We want to update the user through the pipeline, but the ActivityPub diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 31f3b3a8d..338a35052 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -171,6 +171,16 @@ def create(%{assigns: %{user: user}, body_params: %{status: _} = params} = conn, Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id]) |> put_application(conn) + expires_in_seconds = + if is_nil(user.status_ttl_days), + do: nil, + else: 60 * 60 * 24 * user.status_ttl_days + + params = + if is_nil(expires_in_seconds), + do: params, + else: Map.put(params, :expires_in, expires_in_seconds) + with {:ok, activity} <- CommonAPI.post(user, params) do try_render(conn, "show.json", activity: activity, diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a04ffaaf3..653a50e20 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -287,7 +287,8 @@ defp do_render("show.json", %{user: user} = opts) do }, last_status_at: user.last_status_at, akkoma: %{ - instance: render("instance.json", %{instance: instance}) + instance: render("instance.json", %{instance: instance}), + status_ttl_days: user.status_ttl_days }, # Pleroma extensions # Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 929641b84..cc58f803e 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -65,7 +65,7 @@ defp get_replied_to_activities(activities) do # This should be removed in a future version of Pleroma. Pleroma-FE currently # depends on this field, as well. defp get_context_id(%{data: %{"context" => context}}) when is_binary(context) do - use Bitwise + import Bitwise :erlang.crc32(context) |> band(bnot(0x8000_0000)) diff --git a/priv/repo/migrations/20220905011454_generate_unset_user_keys.exs b/priv/repo/migrations/20220905011454_generate_unset_user_keys.exs index 43bc7100b..3b04bb4f3 100644 --- a/priv/repo/migrations/20220905011454_generate_unset_user_keys.exs +++ b/priv/repo/migrations/20220905011454_generate_unset_user_keys.exs @@ -14,14 +14,14 @@ def change do from(u in User, where: u.local == true, where: is_nil(u.keys), - select: u + select: u.id ) Repo.stream(query) |> Enum.each(fn user -> with {:ok, pem} <- Keys.generate_rsa_pem() do - Ecto.Changeset.cast(user, %{keys: pem}, [:keys]) - |> Repo.update() + Ecto.Changeset.cast(%User{id: user}, %{keys: pem}, [:keys]) + |> Repo.update(returning: false) end end) end diff --git a/priv/repo/migrations/20221128103145_add_per_user_post_expiry.exs b/priv/repo/migrations/20221128103145_add_per_user_post_expiry.exs new file mode 100644 index 000000000..95c585cf4 --- /dev/null +++ b/priv/repo/migrations/20221128103145_add_per_user_post_expiry.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddPerUserPostExpiry do + use Ecto.Migration + + def change do + alter table(:users) do + add(:status_ttl_days, :integer, null: true) + end + end +end diff --git a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs index ea6ace69f..cda4c9e03 100644 --- a/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs +++ b/test/pleroma/web/mastodon_api/controllers/status_controller_test.exs @@ -126,6 +126,35 @@ test "posting a status", %{conn: conn} do ) end + test "automatically setting a post expiry if status_ttl_days is set" do + user = insert(:user, status_ttl_days: 1) + %{user: _user, token: _token, conn: conn} = oauth_access(["write:statuses"], user: user) + + conn = + conn + |> put_req_header("content-type", "application/json") + |> post("api/v1/statuses", %{ + "status" => "aa chikichiki banban" + }) + + assert %{"id" => id} = json_response_and_validate_schema(conn, 200) + + activity = Activity.get_by_id_with_object(id) + {:ok, expires_at, _} = DateTime.from_iso8601(activity.data["expires_at"]) + + assert Timex.diff( + expires_at, + DateTime.utc_now(), + :hours + ) == 23 + + assert_enqueued( + worker: Pleroma.Workers.PurgeExpiredActivity, + args: %{activity_id: id}, + scheduled_at: DateTime.add(DateTime.utc_now(), 1 * 60 * 60 * 24) + ) + end + test "it fails to create a status if `expires_in` is less or equal than an hour", %{ conn: conn } do diff --git a/test/pleroma/web/mastodon_api/update_credentials_test.exs b/test/pleroma/web/mastodon_api/update_credentials_test.exs index 606467fa9..435782d0a 100644 --- a/test/pleroma/web/mastodon_api/update_credentials_test.exs +++ b/test/pleroma/web/mastodon_api/update_credentials_test.exs @@ -209,6 +209,26 @@ test "updates the user's name", %{conn: conn} do assert update_activity.data["object"]["name"] == "markorepairs" end + test "updates the user's default post expiry", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"status_ttl_days" => "1"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert user_data["akkoma"]["status_ttl_days"] == 1 + end + + test "resets the user's default post expiry", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"status_ttl_days" => "-1"}) + + assert user_data = json_response_and_validate_schema(conn, 200) + assert is_nil(user_data["akkoma"]["status_ttl_days"]) + end + + test "does not allow negative integers other than -1 for TTL", %{conn: conn} do + conn = patch(conn, "/api/v1/accounts/update_credentials", %{"status_ttl_days" => "-2"}) + + assert user_data = json_response_and_validate_schema(conn, 403) + end + test "updates the user's AKAs", %{conn: conn} do conn = patch(conn, "/api/v1/accounts/update_credentials", %{ diff --git a/test/pleroma/web/mastodon_api/views/account_view_test.exs b/test/pleroma/web/mastodon_api/views/account_view_test.exs index f4a5f4d50..c9036d67d 100644 --- a/test/pleroma/web/mastodon_api/views/account_view_test.exs +++ b/test/pleroma/web/mastodon_api/views/account_view_test.exs @@ -37,7 +37,8 @@ test "Represent a user account" do inserted_at: ~N[2017-08-15 15:47:06.597036], emoji: %{"karjalanpiirakka" => "/file.png"}, raw_bio: "valid html. a\nb\nc\nd\nf '&<>\"", - also_known_as: ["https://shitposter.zone/users/shp"] + also_known_as: ["https://shitposter.zone/users/shp"], + status_ttl_days: 5 }) insert(:instance, %{host: "example.com", nodeinfo: %{version: "2.1"}}) @@ -61,7 +62,8 @@ test "Represent a user account" do "version" => "2.1" }, favicon: nil - } + }, + status_ttl_days: 5 }, avatar: "http://localhost:4001/images/avi.png", avatar_static: "http://localhost:4001/images/avi.png", @@ -243,7 +245,8 @@ test "Represent a Service(bot) account" do name: "localhost", favicon: "http://localhost:4001/favicon.png", nodeinfo: %{version: "2.0"} - } + }, + status_ttl_days: nil }, pleroma: %{ ap_id: user.ap_id,