Drop broken list addressing feature

This feature was both conceptually broken and through bitrotting
the implementation was also buggy with the handling of certain
list-post interactions just crashing.

Remote servers had no way to know who belongs to a list and thus
posts basically showed just up as weird DM threads with different
participants on each instance. And while on the local instance
addition and removal from a listed grated and revoked post
access retroactively, it never acted retroactively on remotes.

Notably our "activity_visibility" database function also didn’t
know about "list visibility" instead treating them as direct messages.

Furthermore no known client actualy allows creating such messages
and the lack of complaints about the accumulutaed bugs supports
the absence of any users.

Given this there seems no point in fixing the implementation.
To reduce complexity of visibility handling it will be dropped instead.
Note, a similar effect with less federation weirdness can already be achieved
client-side using the explicit-addressing feature originally introduced in
https://git.pleroma.social/pleroma/pleroma/-/merge_requests/1239.

Ref: https://akkoma.dev/AkkomaGang/akkoma/issues/812
This commit is contained in:
Oneric 2025-11-02 15:38:02 +01:00
parent d96c6f438d
commit 271110c1a5
12 changed files with 39 additions and 122 deletions

View file

@ -32,7 +32,7 @@ Home, public, hashtag & list timelines further accept:
## Statuses
- `visibility`: has additional possible values `list` and `local` (for local-only statuses)
- `visibility`: has additional possible value `local` (for local-only statuses)
- `emoji_reactions`: additional field since Akkoma 3.2.0; identical to `pleroma/emoji_reactions`
Has these additional fields under the `pleroma` object:

View file

@ -267,17 +267,33 @@ special meaning to the potential local-scope identifier.
however those are also shown publicly on the local web interface
and are thus visible to non-members.
## List post scope
Messages originally addressed to a custom list will contain
a `listMessage` field with an unresolvable pseudo ActivityPub id.
# Deprecated and Removed Extensions
The following extensions were used in the past but have been dropped.
Documentation is retained here as a reference and since old objects might
still contains related fields.
## List post scope
Messages originally addressed to a custom list will contain
a `listMessage` field with an unresolvable pseudo ActivityPub id.
!!! note
The concept did not work out too well in practice with even remote servers
recognising the `listMessage` extension being unaware of the state of the
list and resulting weird desyncs in thread display and handling between
servers.
As it was it also never found its way in any known clients or frontends.
A more consistent superset of what this was able to actually do
can be achieved without ActivityPub extensions by explicitly addressing
all intended participants without inline mentions.
While true federated and moderated "lists" or "groups"
will need more work and a different approach.
Thus suport for it was removed and it is recommended
to not create any new implementation of it.
## Actor endpoints
The following endpoints used to be present:

View file

@ -53,25 +53,11 @@ def is_direct?(activity) do
!is_public?(activity) && !is_private?(activity)
end
def is_list?(%{data: %{"listMessage" => _}}), do: true
def is_list?(_), do: false
@spec visible_for_user?(Object.t() | Activity.t() | nil, User.t() | nil) :: boolean()
def visible_for_user?(%Object{data: %{"type" => "Tombstone"}}, _), do: false
def visible_for_user?(%Activity{actor: ap_id}, %User{ap_id: ap_id}), do: true
def visible_for_user?(%Object{data: %{"actor" => ap_id}}, %User{ap_id: ap_id}), do: true
def visible_for_user?(nil, _), do: false
def visible_for_user?(%Activity{data: %{"listMessage" => _}}, nil), do: false
def visible_for_user?(
%Activity{data: %{"listMessage" => list_ap_id}} = activity,
%User{} = user
) do
user.ap_id in activity.data["to"] ||
list_ap_id
|> Pleroma.List.get_by_ap_id()
|> Pleroma.List.member?(user)
end
def visible_for_user?(%{__struct__: module} = message, nil)
when module in [Activity, Object] do
@ -141,9 +127,6 @@ def get_visibility(object) do
object.data["directMessage"] == true ->
"direct"
is_binary(object.data["listMessage"]) ->
"list"
length(cc) > 0 ->
"private"

View file

@ -553,10 +553,9 @@ defp create_request do
nullable: true,
anyOf: [
VisibilityScope,
%Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
%Schema{type: :string, description: "scope name", example: "unlisted"}
],
description:
"Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`"
description: "Visibility of the posted status."
},
expires_in: %Schema{
nullable: true,

View file

@ -9,6 +9,6 @@ defmodule Pleroma.Web.ApiSpec.Schemas.VisibilityScope do
title: "VisibilityScope",
description: "Status visibility",
type: :string,
enum: ["public", "unlisted", "local", "private", "direct", "list"]
enum: ["public", "unlisted", "local", "private", "direct"]
})
end

View file

@ -300,11 +300,6 @@ def get_visibility(%{visibility: visibility}, in_reply_to, _)
when visibility in ~w{public local unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do
visibility = {:list, String.to_integer(list_id)}
{visibility, get_replied_to_visibility(in_reply_to)}
end
def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do
visibility = get_replied_to_visibility(in_reply_to)
{visibility, visibility}

View file

@ -298,7 +298,6 @@ defp changes(draft) do
object: draft.object,
additional: additional
}
|> Utils.maybe_add_list_data(draft.user, draft.visibility)
%__MODULE__{draft | changes: changes}
end

View file

@ -118,31 +118,12 @@ def get_to_and_cc_for_visibility("direct", _, _, mentions) do
{mentions, []}
end
def get_to_and_cc_for_visibility({:list, _}, _, _, mentions) do
{mentions, []}
end
def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to)
end
def get_addressed_users(mentioned_users, _), do: mentioned_users
def maybe_add_list_data(activity_params, user, {:list, list_id}) do
case Pleroma.List.get(list_id, user) do
%Pleroma.List{} = list ->
activity_params
|> put_in([:additional, "bcc"], [list.ap_id])
|> put_in([:additional, "listMessage"], list.ap_id)
|> put_in([:object, "listMessage"], list.ap_id)
_ ->
activity_params
end
end
def maybe_add_list_data(activity_params, _, _), do: activity_params
def make_poll_data(%{"poll" => %{"expires_in" => expires_in}} = data)
when is_binary(expires_in) do
# In some cases mastofe sends out strings instead of integers

View file

@ -36,12 +36,18 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
{:ok, local} = CommonAPI.post(user, %{status: "@#{mentioned.nickname}", visibility: "local"})
{:ok, list} =
# list visibility is no longer supported, but we want to check any
# leftover entreis are handled sensibly, so we nned to manually fix up the data
{:ok, list_activity} =
CommonAPI.post(user, %{
status: "@#{mentioned.nickname}",
visibility: "list:#{list.id}"
visibility: "direct"
})
list_object = Object.normalize(list_activity)
{:ok, list_object} = Object.update_data(list_object, %{"listMessage" => list.ap_id})
list_activity = %Activity{list_activity | object: list_object}
%{
public: public,
private: private,
@ -51,7 +57,7 @@ defmodule Pleroma.Web.ActivityPub.VisibilityTest do
mentioned: mentioned,
following: following,
unrelated: unrelated,
list: list,
list: list_activity,
local: local,
remote: remote
}
@ -69,8 +75,8 @@ test "is_direct?", %{
refute Visibility.is_direct?(public)
refute Visibility.is_direct?(private)
refute Visibility.is_direct?(unlisted)
assert Visibility.is_direct?(list)
refute Visibility.is_direct?(local)
assert Visibility.is_direct?(list)
end
test "is_public?", %{
@ -105,22 +111,6 @@ test "is_private?", %{
refute Visibility.is_private?(local)
end
test "is_list?", %{
public: public,
private: private,
direct: direct,
unlisted: unlisted,
list: list,
local: local
} do
refute Visibility.is_list?(direct)
refute Visibility.is_list?(public)
refute Visibility.is_list?(private)
refute Visibility.is_list?(unlisted)
assert Visibility.is_list?(list)
refute Visibility.is_list?(local)
end
test "visible_for_user? Activity", %{
public: public,
private: private,
@ -177,9 +167,6 @@ test "visible_for_user? Activity", %{
refute Visibility.visible_for_user?(direct, nil)
refute Visibility.visible_for_user?(local, nil)
# Visible for a list member
assert Visibility.visible_for_user?(list, unrelated)
# Local not visible to remote user
refute Visibility.visible_for_user?(local, remote)
end
@ -270,15 +257,16 @@ test "get_visibility", %{
assert Visibility.get_visibility(private) == "private"
assert Visibility.get_visibility(direct) == "direct"
assert Visibility.get_visibility(unlisted) == "unlisted"
assert Visibility.get_visibility(list) == "list"
# legacy, no longer supported visibility type doesn't leak out now
assert Visibility.get_visibility(list) == "direct"
end
test "get_visibility with directMessage flag" do
assert Visibility.get_visibility(%{data: %{"directMessage" => true}}) == "direct"
end
test "get_visibility with listMessage flag" do
assert Visibility.get_visibility(%{data: %{"listMessage" => ""}}) == "list"
test "get_visibility treats legacy list messages as direct" do
assert Visibility.get_visibility(%{data: %{"listMessage" => ""}}) == "direct"
end
describe "entire_thread_visible_for_user?/2" do

View file

@ -609,27 +609,6 @@ test "returns [] when not pass media_ids" do
end
end
describe "maybe_add_list_data/3" do
test "adds list params when found user list" do
user = insert(:user)
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user)
assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) ==
%{
additional: %{"bcc" => [list.ap_id], "listMessage" => list.ap_id},
object: %{"listMessage" => list.ap_id}
}
end
test "returns original params when list not found" do
user = insert(:user)
{:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", insert(:user))
assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) ==
%{additional: %{}, object: %{}}
end
end
describe "maybe_add_attachments/3" do
test "returns parsed results when attachment_links is false" do
assert Utils.maybe_add_attachments(

View file

@ -543,17 +543,6 @@ test "replying with a direct message will NOT auto-add the author of the reply t
refute user.ap_id in secret_answer.recipients
end
test "it allows to address a list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
assert activity.data["bcc"] == [list.ap_id]
assert activity.recipients == [list.ap_id, user.ap_id]
assert activity.data["listMessage"] == list.ap_id
end
test "it adds the htmlMFM term to MFM posts and properly processes it" do
user = insert(:user)

View file

@ -942,18 +942,6 @@ test "does not embed a relationship in the account in reposts" do
assert_schema(result, "Status", Pleroma.Web.ApiSpec.spec())
end
test "visibility/list" do
user = insert(:user)
{:ok, list} = Pleroma.List.create("foo", user)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
status = StatusView.render("show.json", activity: activity)
assert status.visibility == "list"
end
test "has a field for parent visibility" do
user = insert(:user)
poster = insert(:user)