## 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)
- Relays from akkoma are now off by default
- NormalizeMarkup MRF is now on by default
+- Follow/Block/Mute imports now spin off into *n* tasks to avoid the oban timeout
## 2022.11
+++ /dev/null
-#!/bin/sh
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-project_id="74"
-project_branch="rebase/glitch-soc"
-static_dir="instance/static"
-# For bundling:
-# project_branch="pleroma"
-# static_dir="priv/static"
-
-if [ ! -d "${static_dir}" ]
-then
- echo "Error: ${static_dir} directory is missing, are you sure you are running this script at the root of pleroma’s repository?"
- exit 1
-fi
-
-last_modified="$(curl --fail -s -I 'https://git.pleroma.social/api/v4/projects/'${project_id}'/jobs/artifacts/'${project_branch}'/download?job=build' | grep '^Last-Modified:' | cut -d: -f2-)"
-
-echo "branch:${project_branch}"
-echo "Last-Modified:${last_modified}"
-
-artifact="mastofe.zip"
-
-if [ "${last_modified}x" = "x" ]
-then
- echo "ERROR: Couldn't get the modification date of the latest build archive, maybe it expired, exiting..."
- exit 1
-fi
-
-if [ -e mastofe.timestamp ] && [ "$(cat mastofe.timestamp)" = "${last_modified}" ]
-then
- echo "MastoFE is up-to-date, exiting..."
- exit 0
-fi
-
-curl --fail -c - "https://git.pleroma.social/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=build" -o "${artifact}" || exit
-
-# TODO: Update the emoji as well
-rm -fr "${static_dir}/sw.js" "${static_dir}/packs" || exit
-unzip -q "${artifact}" || exit
-
-cp public/assets/sw.js "${static_dir}/sw.js" || exit
-cp -r public/packs "${static_dir}/packs" || exit
-
-echo "${last_modified}" > mastofe.timestamp
-rm -fr public
-rm -i "${artifact}"
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,
:pleroma_settings_store,
:is_discoverable,
:actor_type,
- :disclose_client
+ :disclose_client,
+ :status_ttl_days
]
)
|> unique_constraint(:nickname)
|> 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)})
require Logger
@spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()}
- def perform(:mutes_import, %User{} = user, [_ | _] = identifiers) do
- Enum.map(
- identifiers,
- fn identifier ->
- with {:ok, %User{} = muted_user} <- User.get_or_fetch(identifier),
- {:ok, _} <- User.mute(user, muted_user) do
- muted_user
- else
- error -> handle_error(:mutes_import, identifier, error)
- end
- end
- )
+ def perform(:mutes_import, %User{} = user, identifier) do
+ with {:ok, %User{} = muted_user} <- User.get_or_fetch(identifier),
+ {:ok, _} <- User.mute(user, muted_user) do
+ muted_user
+ else
+ error -> handle_error(:mutes_import, identifier, error)
+ end
end
- def perform(:blocks_import, %User{} = blocker, [_ | _] = identifiers) do
- Enum.map(
- identifiers,
- fn identifier ->
- with {:ok, %User{} = blocked} <- User.get_or_fetch(identifier),
- {:ok, _block} <- CommonAPI.block(blocker, blocked) do
- blocked
- else
- error -> handle_error(:blocks_import, identifier, error)
- end
- end
- )
+ def perform(:blocks_import, %User{} = blocker, identifier) do
+ with {:ok, %User{} = blocked} <- User.get_or_fetch(identifier),
+ {:ok, _block} <- CommonAPI.block(blocker, blocked) do
+ blocked
+ else
+ error -> handle_error(:blocks_import, identifier, error)
+ end
end
- def perform(:follow_import, %User{} = follower, [_ | _] = identifiers) do
- Enum.map(
- identifiers,
- fn identifier ->
- with {:ok, %User{} = followed} <- User.get_or_fetch(identifier),
- {:ok, follower, followed} <- User.maybe_direct_follow(follower, followed),
- {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
- followed
- else
- error -> handle_error(:follow_import, identifier, error)
- end
- end
- )
+ def perform(:follow_import, %User{} = follower, identifier) do
+ with {:ok, %User{} = followed} <- User.get_or_fetch(identifier),
+ {:ok, follower, followed} <- User.maybe_direct_follow(follower, followed),
+ {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do
+ followed
+ else
+ error -> handle_error(:follow_import, identifier, error)
+ end
end
def perform(_, _, _), do: :ok
error
end
- def blocks_import(%User{} = blocker, [_ | _] = identifiers) do
- BackgroundWorker.enqueue(
- "blocks_import",
- %{"user_id" => blocker.id, "identifiers" => identifiers}
+ defp enqueue_many(op, user, identifiers) do
+ Enum.map(
+ identifiers,
+ fn identifier ->
+ BackgroundWorker.enqueue(op, %{"user_id" => user.id, "identifier" => identifier})
+ end
)
end
+ def blocks_import(%User{} = blocker, [_ | _] = identifiers) do
+ enqueue_many("blocks_import", blocker, identifiers)
+ end
+
def follow_import(%User{} = follower, [_ | _] = identifiers) do
- BackgroundWorker.enqueue(
- "follow_import",
- %{"user_id" => follower.id, "identifiers" => identifiers}
- )
+ enqueue_many("follow_import", follower, identifiers)
end
def mutes_import(%User{} = user, [_ | _] = identifiers) do
- BackgroundWorker.enqueue(
- "mutes_import",
- %{"user_id" => user.id, "identifiers" => identifiers}
- )
+ enqueue_many("mutes_import", user, identifiers)
end
end
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,
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
}
}
},
+ akkoma: %Schema{
+ type: :object,
+ properties: %{
+ note_ttl_days: %Schema{type: :integer}
+ }
+ },
source: %Schema{
type: :object,
properties: %{
value -> {:ok, value}
end
+ status_ttl_days_value = fn
+ -1 -> {:ok, nil}
+ value -> {:ok, value}
+ end
+
user_params =
[
:no_rich_text,
# 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
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,
},
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
# 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))
User.perform(:force_password_reset, user)
end
- def perform(%Job{args: %{"op" => op, "user_id" => user_id, "identifiers" => identifiers}})
+ def perform(%Job{args: %{"op" => op, "user_id" => user_id, "identifier" => identifier}})
when op in ["blocks_import", "follow_import", "mutes_import"] do
user = User.get_cached_by_id(user_id)
- {:ok, User.Import.perform(String.to_atom(op), user, identifiers)}
+ {:ok, User.Import.perform(String.to_atom(op), user, identifier)}
end
def perform(%Job{
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
--- /dev/null
+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
text = "I love :firefox:"
expected_result =
- "I love <img class=\"emoji\" alt=\"firefox\" title=\"firefox\" src=\"/emoji/Firefox.gif\"/>"
+ "I love <img alt=\"firefox\" title=\"firefox\" src=\"/emoji/Firefox.gif\"/>"
assert Formatter.emojify(text) == expected_result
end
user3.nickname
]
- {:ok, job} = User.Import.follow_import(user1, identifiers)
+ [{:ok, job1}, {:ok, job2}] = User.Import.follow_import(user1, identifiers)
+
+ assert {:ok, result} = ObanHelpers.perform(job1)
+ assert result == refresh_record(user2)
+
+ assert {:ok, result} = ObanHelpers.perform(job2)
+ assert result == refresh_record(user3)
- assert {:ok, result} = ObanHelpers.perform(job)
- assert is_list(result)
- assert result == [refresh_record(user2), refresh_record(user3)]
assert User.following?(user1, user2)
assert User.following?(user1, user3)
end
user3.nickname
]
- {:ok, job} = User.Import.blocks_import(user1, identifiers)
+ [{:ok, job1}, {:ok, job2}] = User.Import.blocks_import(user1, identifiers)
+
+ assert {:ok, result} = ObanHelpers.perform(job1)
+ assert result == user2
+
+ assert {:ok, result} = ObanHelpers.perform(job2)
+ assert result == user3
- assert {:ok, result} = ObanHelpers.perform(job)
- assert is_list(result)
- assert result == [user2, user3]
assert User.blocks?(user1, user2)
assert User.blocks?(user1, user3)
end
user3.nickname
]
- {:ok, job} = User.Import.mutes_import(user1, identifiers)
+ [{:ok, job1}, {:ok, job2}] = User.Import.mutes_import(user1, identifiers)
+
+ assert {:ok, result} = ObanHelpers.perform(job1)
+ assert result == user2
+
+ assert {:ok, result} = ObanHelpers.perform(job2)
+ assert result == user3
- assert {:ok, result} = ObanHelpers.perform(job)
- assert is_list(result)
- assert result == [user2, user3]
assert User.mutes?(user1, user2)
assert User.mutes?(user1, user3)
end
)
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
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", %{
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"}})
"version" => "2.1"
},
favicon: nil
- }
+ },
+ status_ttl_days: 5
},
avatar: "http://localhost:4001/images/avi.png",
avatar_static: "http://localhost:4001/images/avi.png",
name: "localhost",
favicon: "http://localhost:4001/favicon.png",
nodeinfo: %{version: "2.0"}
- }
+ },
+ status_ttl_days: nil
},
pleroma: %{
ap_id: user.ap_id,
|> json_response_and_validate_schema(200)
assert [{:ok, job_result}] = ObanHelpers.perform_all()
- assert job_result == [refresh_record(user2)]
- assert [%Pleroma.User{follower_count: 1}] = job_result
+ assert job_result == refresh_record(user2)
+ assert %Pleroma.User{follower_count: 1} = job_result
end
end
|> post("/api/pleroma/follow_import", %{"list" => identifiers})
|> json_response_and_validate_schema(200)
- assert [{:ok, job_result}] = ObanHelpers.perform_all()
- assert job_result == Enum.map(users, &refresh_record/1)
+ job_results = Enum.map(ObanHelpers.perform_all(), fn {:ok, result} -> result end)
+ assert job_results == Enum.map(users, &refresh_record/1)
end
end
})
|> json_response_and_validate_schema(200)
- assert [{:ok, job_result}] = ObanHelpers.perform_all()
- assert job_result == users
+ job_results = Enum.map(ObanHelpers.perform_all(), fn {:ok, result} -> result end)
+ assert job_results == users
end
end
|> post("/api/pleroma/blocks_import", %{"list" => identifiers})
|> json_response_and_validate_schema(200)
- assert [{:ok, job_result}] = ObanHelpers.perform_all()
- assert job_result == users
+ job_results = Enum.map(ObanHelpers.perform_all(), fn {:ok, result} -> result end)
+ assert job_results == users
end
end
|> post("/api/pleroma/mutes_import", %{"list" => "#{user2.ap_id}"})
|> json_response_and_validate_schema(200)
- assert [{:ok, job_result}] = ObanHelpers.perform_all()
- assert job_result == [user2]
+ job_results = Enum.map(ObanHelpers.perform_all(), fn {:ok, result} -> result end)
+ assert job_results == [user2]
assert Pleroma.User.mutes?(user, user2)
end
- test "it imports mutes users from file", %{user: user, conn: conn} do
+ test "it imports muted users from file", %{user: user, conn: conn} do
users = [user2, user3] = insert_list(2, :user)
with_mocks([
})
|> json_response_and_validate_schema(200)
- assert [{:ok, job_result}] = ObanHelpers.perform_all()
- assert job_result == users
+ job_results = Enum.map(ObanHelpers.perform_all(), fn {:ok, result} -> result end)
+ assert job_results == users
assert Enum.all?(users, &Pleroma.User.mutes?(user, &1))
end
end
|> post("/api/pleroma/mutes_import", %{"list" => identifiers})
|> json_response_and_validate_schema(200)
- assert [{:ok, job_result}] = ObanHelpers.perform_all()
- assert job_result == users
+ job_results = Enum.map(ObanHelpers.perform_all(), fn {:ok, result} -> result end)
+ assert job_results == users
assert Enum.all?(users, &Pleroma.User.mutes?(user, &1))
end
end