Merge branch 'develop' of https://akkoma.dev/AkkomaGang/akkoma into froth-akkoma

This commit is contained in:
Sam Therapy 2023-05-26 13:47:43 +02:00
commit 2172cb0e38
Signed by: sam
GPG key ID: 4D8B07C18F31ACBD
20 changed files with 381 additions and 15 deletions

View file

@ -4,10 +4,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
## 2023.05
## Added
- Custom options for users to accept/reject private messages
- options: everybody, nobody, people\_i\_follow
- MRF to reject notes from accounts newer than a given age
- this will have the side-effect of rejecting legitimate messages if your
post gets boosted outside of your local bubble and people your instance
does not know about reply to it.
## Fixed
- Support for `streams` public key URIs
- Bookmarks are cleaned up on DB prune now
## Security
- Fixed mediaproxy being a bit of a silly billy
## 2023.04

View file

@ -429,6 +429,8 @@
config :pleroma, :mrf_follow_bot, follower_nickname: nil
config :pleroma, :mrf_reject_newly_created_account_notes, age: 86_400
config :pleroma, :rich_media,
enabled: true,
ignore_hosts: [],

View file

@ -39,9 +39,9 @@ def translate(string, from_language, to_language) do
detected =
if Map.has_key?(body, "detectedLanguage") do
get_in(body, ["detectedLanguage", "language"])
else
else
from_language || ""
end
end
{:ok, detected, translated}
else

View file

@ -251,6 +251,7 @@ defp build_resp_headers(headers, opts) do
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|> build_resp_cache_headers(opts)
|> build_resp_content_disposition_header(opts)
|> build_csp_headers()
|> Keyword.merge(Keyword.get(opts, :resp_headers, []))
end
@ -316,6 +317,10 @@ defp build_resp_content_disposition_header(headers, opts) do
end
end
defp build_csp_headers(headers) do
List.keystore(headers, "content-security-policy", 0, {"content-security-policy", "sandbox"})
end
defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do
with {_, size} <- List.keyfind(headers, "content-length", 0),
{size, _} <- Integer.parse(size),

View file

@ -160,6 +160,11 @@ defmodule Pleroma.User do
field(:language, :string)
field(:status_ttl_days, :integer, default: nil)
field(:accepts_direct_messages_from, Ecto.Enum,
values: [:everybody, :people_i_follow, :nobody],
default: :everybody
)
embeds_one(
:notification_settings,
Pleroma.User.NotificationSetting,
@ -550,7 +555,8 @@ def update_changeset(struct, params \\ %{}) do
:actor_type,
:accepts_chat_messages,
:disclose_client,
:status_ttl_days
:status_ttl_days,
:accepts_direct_messages_from
]
)
|> unique_constraint(:nickname)
@ -2748,4 +2754,16 @@ def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
not is_nil(HashtagFollow.get(user, hashtag))
end
def accepts_direct_messages?(
%User{accepts_direct_messages_from: :people_i_follow} = receiver,
%User{} = sender
) do
User.following?(receiver, sender)
end
def accepts_direct_messages?(%User{accepts_direct_messages_from: :everybody}, _), do: true
def accepts_direct_messages?(%User{accepts_direct_messages_from: :nobody}, _),
do: false
end

View file

@ -141,7 +141,8 @@ def get_policies do
|> Enum.concat([
Pleroma.Web.ActivityPub.MRF.HashtagPolicy,
Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy,
Pleroma.Web.ActivityPub.MRF.NormalizeMarkup
Pleroma.Web.ActivityPub.MRF.NormalizeMarkup,
Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy
])
|> Enum.uniq()
end

View file

@ -0,0 +1,65 @@
defmodule Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.User
require Pleroma.Constants
@moduledoc """
Removes entries from the "To" field from direct messages if the user has requested to not
allow direct messages
"""
@impl true
def filter(
%{
"type" => "Create",
"actor" => actor,
"object" => %{
"type" => "Note"
}
} = activity
) do
with recipients <- Map.get(activity, "to", []),
cc <- Map.get(activity, "cc", []),
true <- is_direct?(recipients, cc),
sender <- User.get_cached_by_ap_id(actor) do
new_to =
Enum.filter(recipients, fn recv ->
should_include?(sender, recv)
end)
{:ok,
activity
|> Map.put("to", new_to)
|> maybe_replace_object_to(new_to)}
else
_ ->
{:ok, activity}
end
end
@impl true
def filter(object), do: {:ok, object}
@impl true
def describe, do: {:ok, %{}}
defp should_include?(sender, receiver_ap_id) do
with %User{local: true} = receiver <- User.get_cached_by_ap_id(receiver_ap_id) do
User.accepts_direct_messages?(receiver, sender)
else
_ -> true
end
end
defp maybe_replace_object_to(%{"object" => %{"to" => _}} = activity, to) do
Kernel.put_in(activity, ["object", "to"], to)
end
defp maybe_replace_object_to(other, _), do: other
defp is_direct?(to, cc) do
!(Enum.member?(to, Pleroma.Constants.as_public()) ||
Enum.member?(cc, Pleroma.Constants.as_public()))
end
end

View file

@ -0,0 +1,50 @@
defmodule Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.User
@moduledoc """
Rejects notes from accounts that were created below a certain threshold of time ago
"""
@impl true
def filter(
%{
"type" => type,
"actor" => actor
} = activity
)
when type in ["Note", "Create"] do
min_age = Pleroma.Config.get([:mrf_reject_newly_created_account_notes, :age])
with %User{local: false} = user <- Pleroma.User.get_cached_by_ap_id(actor),
true <- Timex.diff(Timex.now(), user.inserted_at, :seconds) < min_age do
{:reject, "[RejectNewlyCreatedAccountNotesPolicy] Account created too recently"}
else
_ -> {:ok, activity}
end
end
@impl true
def filter(object), do: {:ok, object}
@impl true
def describe, do: {:ok, %{}}
@impl true
def config_description do
%{
key: :mrf_reject_newly_created_account_notes,
related_policy: "Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy",
label: "MRF Reject New Accounts",
description: "Reject notes from accounts created too recently",
children: [
%{
key: :age,
type: :integer,
description: "Time below which to reject (in seconds)",
suggestions: [86_400]
}
]
}
end
end

View file

@ -713,6 +713,16 @@ defp update_credentials_request do
nullable: true,
description:
"Number of days after which statuses will be deleted. Set to -1 to disable."
},
accepts_direct_messages_from: %Schema{
type: :string,
enum: [
"everybody",
"nobody",
"people_i_follow"
],
nullable: true,
description: "Who to accept DMs from"
}
},
example: %{
@ -734,7 +744,8 @@ defp update_credentials_request do
also_known_as: ["https://foo.bar/users/foo"],
discoverable: false,
actor_type: "Person",
status_ttl_days: 30
status_ttl_days: 30,
accepts_direct_messages_from: "everybody"
}
}
end

View file

@ -225,6 +225,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> 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)
|> Maps.put_if_present(:accepts_direct_messages_from, params[:accepts_direct_messages_from])
IO.inspect(user_params)
# What happens here:

View file

@ -356,6 +356,7 @@ defp maybe_put_settings(
|> Kernel.put_in([:source, :privacy], user.default_scope)
|> Kernel.put_in([:source, :pleroma, :show_role], user.show_role)
|> Kernel.put_in([:source, :pleroma, :no_rich_text], user.no_rich_text)
|> Kernel.put_in([:accepts_direct_messages_from], user.accepts_direct_messages_from)
end
defp maybe_put_settings(data, _, _, _), do: data

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Mixfile do
def project do
[
app: :pleroma,
version: version("3.8.0"),
version: version("3.9.2"),
elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix] ++ Mix.compilers(),

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddUnfollowedDmRestrictions do
use Ecto.Migration
def change do
alter table(:users) do
add(:accepts_direct_messages_from, :string, default: "everybody")
end
end
end

View file

@ -146,8 +146,7 @@ test "should work when no detected language is received" do
}
end)
assert {:ok, "", "I will crush you"} =
LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "en")
assert {:ok, "", "I will crush you"} = LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "en")
end
end
end

View file

@ -2799,4 +2799,35 @@ test "should not error when trying to unfollow a hashtag twice" do
assert user.followed_hashtags |> Enum.count() == 0
end
end
describe "accepts_direct_messages?/2" do
test "should return true if the recipient follows the sender and has set accept to :people_i_follow" do
recipient =
insert(:user, %{
accepts_direct_messages_from: :people_i_follow
})
sender = insert(:user)
refute User.accepts_direct_messages?(recipient, sender)
CommonAPI.follow(recipient, sender)
assert User.accepts_direct_messages?(recipient, sender)
end
test "should return true if the recipient has set accept to :everyone" do
recipient = insert(:user, %{accepts_direct_messages_from: :everybody})
sender = insert(:user)
assert User.accepts_direct_messages?(recipient, sender)
end
test "should return false if the receipient set accept to :nobody" do
recipient = insert(:user, %{accepts_direct_messages_from: :nobody})
sender = insert(:user)
refute User.accepts_direct_messages?(recipient, sender)
end
end
end

View file

@ -0,0 +1,52 @@
defmodule Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicyTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy
alias Pleroma.User
describe "strips recipients" do
test "when the user denies the direct message" do
sender = insert(:user)
recipient = insert(:user, %{accepts_direct_messages_from: :nobody})
refute User.accepts_direct_messages?(recipient, sender)
message = %{
"actor" => sender.ap_id,
"to" => [recipient.ap_id],
"cc" => [],
"type" => "Create",
"object" => %{
"type" => "Note",
"to" => [recipient.ap_id]
}
}
assert {:ok, %{"to" => [], "object" => %{"to" => []}}} =
DirectMessageDisabledPolicy.filter(message)
end
test "when the user does not deny the direct message" do
sender = insert(:user)
recipient = insert(:user, %{accepts_direct_messages_from: :everybody})
assert User.accepts_direct_messages?(recipient, sender)
message = %{
"actor" => sender.ap_id,
"to" => [recipient.ap_id],
"cc" => [],
"type" => "Create",
"object" => %{
"type" => "Note",
"to" => [recipient.ap_id]
}
}
assert {:ok, message} = DirectMessageDisabledPolicy.filter(message)
assert message["to"] == [recipient.ap_id]
assert message["object"]["to"] == [recipient.ap_id]
end
end
end

View file

@ -0,0 +1,45 @@
defmodule Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicyTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy
describe "reject notes from new accounts" do
test "rejects notes from accounts created more recently than `age`" do
clear_config([:mrf_reject_newly_created_account_notes, :age], 86_400)
sender = insert(:user, %{inserted_at: Timex.now(), local: false})
message = %{
"actor" => sender.ap_id,
"type" => "Create"
}
assert {:reject, _} = RejectNewlyCreatedAccountNotesPolicy.filter(message)
end
test "does not reject notes from accounts created longer ago" do
clear_config([:mrf_reject_newly_created_account_notes, :age], 86_400)
a_day_ago = Timex.shift(Timex.now(), days: -1)
sender = insert(:user, %{inserted_at: a_day_ago, local: false})
message = %{
"actor" => sender.ap_id,
"type" => "Create"
}
assert {:ok, _} = RejectNewlyCreatedAccountNotesPolicy.filter(message)
end
test "does not affect local users" do
clear_config([:mrf_reject_newly_created_account_notes, :age], 86_400)
sender = insert(:user, %{inserted_at: Timex.now(), local: true})
message = %{
"actor" => sender.ap_id,
"type" => "Create"
}
assert {:ok, _} = RejectNewlyCreatedAccountNotesPolicy.filter(message)
end
end
end

View file

@ -102,7 +102,13 @@ test "it works as expected with noop policy" do
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy])
expected = %{
mrf_policies: ["NoOpPolicy", "HashtagPolicy", "InlineQuotePolicy", "NormalizeMarkup"],
mrf_policies: [
"NoOpPolicy",
"HashtagPolicy",
"InlineQuotePolicy",
"NormalizeMarkup",
"DirectMessageDisabledPolicy"
],
mrf_hashtag: %{
federated_timeline_removal: [],
reject: [],
@ -118,7 +124,13 @@ test "it works as expected with mock policy" do
clear_config([:mrf, :policies], [MRFModuleMock])
expected = %{
mrf_policies: ["MRFModuleMock", "HashtagPolicy", "InlineQuotePolicy", "NormalizeMarkup"],
mrf_policies: [
"MRFModuleMock",
"HashtagPolicy",
"InlineQuotePolicy",
"NormalizeMarkup",
"DirectMessageDisabledPolicy"
],
mrf_module_mock: "some config data",
mrf_hashtag: %{
federated_timeline_removal: [],

View file

@ -316,7 +316,7 @@ test "it strips internal reactions" do
test "it correctly processes messages with non-array to field" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Jason.decode!()
|> Map.put("to", "https://www.w3.org/ns/activitystreams#Public")
|> put_in(["object", "to"], "https://www.w3.org/ns/activitystreams#Public")
@ -333,7 +333,7 @@ test "it correctly processes messages with non-array to field" do
test "it correctly processes messages with non-array cc field" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Jason.decode!()
|> Map.put("cc", "http://mastodon.example.org/users/admin/followers")
|> put_in(["object", "cc"], "http://mastodon.example.org/users/admin/followers")
@ -346,7 +346,7 @@ test "it correctly processes messages with non-array cc field" do
test "it correctly processes messages with weirdness in address fields" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Jason.decode!()
|> Map.put("cc", ["http://mastodon.example.org/users/admin/followers", ["¿"]])
|> put_in(["object", "cc"], ["http://mastodon.example.org/users/admin/followers", ["¿"]])
@ -412,7 +412,7 @@ test "does NOT schedule background fetching of `replies` beyond max thread depth
activity =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Jason.decode!()
|> Kernel.put_in(["object", "replies"], replies)
%{activity: activity}

View file

@ -734,4 +734,56 @@ test "actor_type field has a higher priority than bot", %{conn: conn} do
assert account["source"]["pleroma"]["actor_type"] == "Person"
end
end
describe "Updating direct message settings" do
setup do: oauth_access(["write:accounts"])
setup :request_content_type
test "changing to :everybody", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{
accepts_direct_messages_from: "everybody"
})
|> json_response_and_validate_schema(200)
assert account["accepts_direct_messages_from"]
assert account["accepts_direct_messages_from"] == "everybody"
assert Pleroma.User.get_by_ap_id(account["url"]).accepts_direct_messages_from == :everybody
end
test "changing to :nobody", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{accepts_direct_messages_from: "nobody"})
|> json_response_and_validate_schema(200)
assert account["accepts_direct_messages_from"]
assert account["accepts_direct_messages_from"] == "nobody"
assert Pleroma.User.get_by_ap_id(account["url"]).accepts_direct_messages_from == :nobody
end
test "changing to :people_i_follow", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{
accepts_direct_messages_from: "people_i_follow"
})
|> json_response_and_validate_schema(200)
assert account["accepts_direct_messages_from"]
assert account["accepts_direct_messages_from"] == "people_i_follow"
assert Pleroma.User.get_by_ap_id(account["url"]).accepts_direct_messages_from ==
:people_i_follow
end
test "changing to an unsupported value", %{conn: conn} do
conn
|> patch("/api/v1/accounts/update_credentials", %{
accepts_direct_messages_from: "unsupported"
})
|> json_response(400)
end
end
end