Add `local` visibility

This commit is contained in:
Egor Kislitsyn 2020-11-11 18:47:57 +04:00
parent c7bcbfbc1d
commit 0118ccb53c
No known key found for this signature in database
GPG Key ID: 1B49CB15B71E7805
14 changed files with 49 additions and 52 deletions

View File

@ -14,7 +14,7 @@ Adding the parameter `reply_visibility` to the public and home timelines queries
## Statuses ## Statuses
- `visibility`: has an additional possible value `list` - `visibility`: has additional possible values `list` and `local` (for local-only statuses)
Has these additional fields under the `pleroma` object: Has these additional fields under the `pleroma` object:
@ -28,7 +28,6 @@ Has these additional fields under the `pleroma` object:
- `thread_muted`: true if the thread the post belongs to is muted - `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint. - `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
- `parent_visible`: If the parent of this post is visible to the user or not. - `parent_visible`: If the parent of this post is visible to the user or not.
- `local_only`: true for local-only, non-federated posts.
## Media Attachments ## Media Attachments
@ -152,10 +151,9 @@ Additional parameters can be added to the JSON body/Form data:
- `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example. - `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
- `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`, `local` or `public`) it can be used to address a List by setting it to `list:LIST_ID`.
- `expires_in`: The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour. - `expires_in`: The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour.
- `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`. - `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`.
- `local_only`: boolean, if set to `true` the post won't be federated.
## GET `/api/v1/statuses` ## GET `/api/v1/statuses`

View File

@ -19,8 +19,6 @@ defmodule Pleroma.Activity do
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
require Pleroma.Constants
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@type actor :: String.t() @type actor :: String.t()
@ -358,12 +356,4 @@ def pinned_by_actor?(%Activity{} = activity) do
actor = user_actor(activity) actor = user_actor(activity)
activity.id in actor.pinned_activities activity.id in actor.pinned_activities
end end
def local_only?(activity) do
recipients = Enum.concat(activity.data["to"], Map.get(activity.data, "cc", []))
public = Pleroma.Constants.as_public()
local = Pleroma.Constants.as_local_public()
Enum.member?(recipients, local) and not Enum.member?(recipients, public)
end
end end

View File

@ -222,7 +222,7 @@ def announce(actor, object, options \\ []) do
actor.ap_id == Relay.ap_id() -> actor.ap_id == Relay.ap_id() ->
[actor.follower_address] [actor.follower_address]
public? and Pleroma.Activity.local_only?(object) -> public? and Visibility.is_local_public?(object) ->
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_local_public()] [actor.follower_address, object.data["actor"], Pleroma.Constants.as_local_public()]
public? -> public? ->

View File

@ -11,6 +11,7 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.SideEffects alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
@spec common_pipeline(map(), keyword()) :: @spec common_pipeline(map(), keyword()) ::
@ -55,7 +56,7 @@ defp maybe_federate(%Activity{} = activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do with {:ok, local} <- Keyword.fetch(meta, :local) do
do_not_federate = meta[:do_not_federate] || !Config.get([:instance, :federating]) do_not_federate = meta[:do_not_federate] || !Config.get([:instance, :federating])
if !do_not_federate and local and not Activity.local_only?(activity) do if !do_not_federate and local and not Visibility.is_local_public?(activity) do
activity = activity =
if object = Keyword.get(meta, :object_data) do if object = Keyword.get(meta, :object_data) do
%{activity | data: Map.put(activity.data, "object", object)} %{activity | data: Map.put(activity.data, "object", object)}

View File

@ -176,7 +176,7 @@ def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) d
with true <- Config.get!([:instance, :federating]), with true <- Config.get!([:instance, :federating]),
true <- type != "Block" || outgoing_blocks, true <- type != "Block" || outgoing_blocks,
false <- Activity.local_only?(activity) do false <- Visibility.is_local_public?(activity) do
Pleroma.Web.Federator.publish(activity) Pleroma.Web.Federator.publish(activity)
end end

View File

@ -23,6 +23,14 @@ def is_public?(data) do
Utils.label_in_message?(Pleroma.Constants.as_local_public(), data) Utils.label_in_message?(Pleroma.Constants.as_local_public(), data)
end end
def is_local_public?(%Object{data: data}), do: is_local_public?(data)
def is_local_public?(%Activity{data: data}), do: is_local_public?(data)
def is_local_public?(data) do
Utils.label_in_message?(Pleroma.Constants.as_local_public(), data) and
not Utils.label_in_message?(Pleroma.Constants.as_public(), data)
end
def is_private?(activity) do def is_private?(activity) do
with false <- is_public?(activity), with false <- is_public?(activity),
%User{follower_address: follower_address} <- %User{follower_address: follower_address} <-
@ -118,6 +126,9 @@ def get_visibility(object) do
Pleroma.Constants.as_public() in cc -> Pleroma.Constants.as_public() in cc ->
"unlisted" "unlisted"
Pleroma.Constants.as_local_public() in to ->
"local"
# this should use the sql for the object's activity # this should use the sql for the object's activity
Enum.any?(to, &String.contains?(&1, "/followers")) -> Enum.any?(to, &String.contains?(&1, "/followers")) ->
"private" "private"

View File

@ -475,10 +475,6 @@ defp create_request do
type: :string, type: :string,
description: description:
"Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`." "Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
},
local_only: %Schema{
type: :boolean,
description: "Post the status as local only"
} }
}, },
example: %{ example: %{

View File

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

View File

@ -359,7 +359,7 @@ def public_announce?(object, _) do
def get_visibility(_, _, %Participation{}), do: {"direct", "direct"} def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}
def get_visibility(%{visibility: visibility}, in_reply_to, _) def get_visibility(%{visibility: visibility}, in_reply_to, _)
when visibility in ~w{public unlisted private direct}, when visibility in ~w{public local unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)} do: {visibility, get_replied_to_visibility(in_reply_to)}
def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do def get_visibility(%{visibility: "list:" <> list_id}, in_reply_to, _) do

View File

@ -65,8 +65,14 @@ def get_to_and_cc(%{in_reply_to_conversation: %Participation{} = participation})
{Enum.map(participation.recipients, & &1.ap_id), []} {Enum.map(participation.recipients, & &1.ap_id), []}
end end
def get_to_and_cc(%{visibility: "public"} = draft) do def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public", "local"] do
to = [public_uri(draft) | draft.mentions]
to =
case visibility do
"public" -> [Pleroma.Constants.as_public() | draft.mentions]
"local" -> [Pleroma.Constants.as_local_public() | draft.mentions]
end
cc = [draft.user.follower_address] cc = [draft.user.follower_address]
if draft.in_reply_to do if draft.in_reply_to do
@ -78,7 +84,7 @@ def get_to_and_cc(%{visibility: "public"} = draft) do
def get_to_and_cc(%{visibility: "unlisted"} = draft) do def get_to_and_cc(%{visibility: "unlisted"} = draft) do
to = [draft.user.follower_address | draft.mentions] to = [draft.user.follower_address | draft.mentions]
cc = [public_uri(draft)] cc = [Pleroma.Constants.as_public()]
if draft.in_reply_to do if draft.in_reply_to do
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc} {Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
@ -103,9 +109,6 @@ def get_to_and_cc(%{visibility: "direct"} = draft) do
def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []} def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []}
defp public_uri(%{params: %{local_only: true}}), do: Pleroma.Constants.as_local_public()
defp public_uri(_), do: Pleroma.Constants.as_public()
def get_addressed_users(_, to) when is_list(to) do def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to) User.get_ap_ids_by_nicknames(to)
end end

View File

@ -368,8 +368,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
direct_conversation_id: direct_conversation_id, direct_conversation_id: direct_conversation_id,
thread_muted: thread_muted?, thread_muted: thread_muted?,
emoji_reactions: emoji_reactions, emoji_reactions: emoji_reactions,
parent_visible: visible_for_user?(reply_to, opts[:for]), parent_visible: visible_for_user?(reply_to, opts[:for])
local_only: Activity.local_only?(activity)
} }
} }
end end

View File

@ -1256,16 +1256,16 @@ test "fallback" do
end end
end end
describe "with `local_only` enabled" do describe "with `local` visibility" do
setup do: clear_config([:instance, :federating], true) setup do: clear_config([:instance, :federating], true)
test "post" do test "post" do
user = insert(:user) user = insert(:user)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", local_only: true}) {:ok, activity} = CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
assert Activity.local_only?(activity) assert Visibility.is_local_public?(activity)
assert_not_called(Pleroma.Web.Federator.publish(activity)) assert_not_called(Pleroma.Web.Federator.publish(activity))
end end
end end
@ -1274,13 +1274,13 @@ test "delete" do
user = insert(:user) user = insert(:user)
{:ok, %Activity{id: activity_id}} = {:ok, %Activity{id: activity_id}} =
CommonAPI.post(user, %{status: "#2hu #2HU", local_only: true}) CommonAPI.post(user, %{status: "#2hu #2HU", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} = assert {:ok, %Activity{data: %{"deleted_activity_id" => ^activity_id}} = activity} =
CommonAPI.delete(activity_id, user) CommonAPI.delete(activity_id, user)
assert Activity.local_only?(activity) assert Visibility.is_local_public?(activity)
assert_not_called(Pleroma.Web.Federator.publish(activity)) assert_not_called(Pleroma.Web.Federator.publish(activity))
end end
end end
@ -1290,13 +1290,13 @@ test "repeat" do
other_user = insert(:user) other_user = insert(:user)
{:ok, %Activity{id: activity_id}} = {:ok, %Activity{id: activity_id}} =
CommonAPI.post(other_user, %{status: "cofe", local_only: true}) CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} = assert {:ok, %Activity{data: %{"type" => "Announce"}} = activity} =
CommonAPI.repeat(activity_id, user) CommonAPI.repeat(activity_id, user)
assert Activity.local_only?(activity) assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity)) refute called(Pleroma.Web.Federator.publish(activity))
end end
end end
@ -1306,7 +1306,7 @@ test "unrepeat" do
other_user = insert(:user) other_user = insert(:user)
{:ok, %Activity{id: activity_id}} = {:ok, %Activity{id: activity_id}} =
CommonAPI.post(other_user, %{status: "cofe", local_only: true}) CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
assert {:ok, _} = CommonAPI.repeat(activity_id, user) assert {:ok, _} = CommonAPI.repeat(activity_id, user)
@ -1314,7 +1314,7 @@ test "unrepeat" do
assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} = assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
CommonAPI.unrepeat(activity_id, user) CommonAPI.unrepeat(activity_id, user)
assert Activity.local_only?(activity) assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity)) refute called(Pleroma.Web.Federator.publish(activity))
end end
end end
@ -1323,13 +1323,13 @@ test "favorite" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", local_only: true}) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} = assert {:ok, %Activity{data: %{"type" => "Like"}} = activity} =
CommonAPI.favorite(user, activity.id) CommonAPI.favorite(user, activity.id)
assert Activity.local_only?(activity) assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity)) refute called(Pleroma.Web.Federator.publish(activity))
end end
end end
@ -1338,13 +1338,13 @@ test "unfavorite" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", local_only: true}) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
{:ok, %Activity{}} = CommonAPI.favorite(user, activity.id) {:ok, %Activity{}} = CommonAPI.favorite(user, activity.id)
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user) assert {:ok, activity} = CommonAPI.unfavorite(activity.id, user)
assert Activity.local_only?(activity) assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity)) refute called(Pleroma.Web.Federator.publish(activity))
end end
end end
@ -1352,13 +1352,13 @@ test "unfavorite" do
test "react_with_emoji" do test "react_with_emoji" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", local_only: true}) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do with_mock Pleroma.Web.Federator, publish: fn _ -> :ok end do
assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} = assert {:ok, %Activity{data: %{"type" => "EmojiReact"}} = activity} =
CommonAPI.react_with_emoji(activity.id, user, "👍") CommonAPI.react_with_emoji(activity.id, user, "👍")
assert Activity.local_only?(activity) assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity)) refute called(Pleroma.Web.Federator.publish(activity))
end end
end end
@ -1366,7 +1366,7 @@ test "react_with_emoji" do
test "unreact_with_emoji" do test "unreact_with_emoji" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", local_only: true}) {:ok, activity} = CommonAPI.post(other_user, %{status: "cofe", visibility: "local"})
{:ok, _reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍") {:ok, _reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
@ -1374,7 +1374,7 @@ test "unreact_with_emoji" do
assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} = assert {:ok, %Activity{data: %{"type" => "Undo"}} = activity} =
CommonAPI.unreact_with_emoji(activity.id, user, "👍") CommonAPI.unreact_with_emoji(activity.id, user, "👍")
assert Activity.local_only?(activity) assert Visibility.is_local_public?(activity)
refute called(Pleroma.Web.Federator.publish(activity)) refute called(Pleroma.Web.Federator.publish(activity))
end end
end end

View File

@ -1749,12 +1749,12 @@ test "posting a local only status" do
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{ |> post("/api/v1/statuses", %{
"status" => "cofe", "status" => "cofe",
"local_only" => "true" "visibility" => "local"
}) })
local = Pleroma.Constants.as_local_public() local = Pleroma.Constants.as_local_public()
assert %{"content" => "cofe", "id" => id, "pleroma" => %{"local_only" => true}} = assert %{"content" => "cofe", "id" => id, "visibility" => "local"} =
json_response(conn_one, 200) json_response(conn_one, 200)
assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id) assert %Activity{id: ^id, data: %{"to" => [^local]}} = Activity.get_by_id(id)

View File

@ -245,8 +245,7 @@ test "a note activity" do
direct_conversation_id: nil, direct_conversation_id: nil,
thread_muted: false, thread_muted: false,
emoji_reactions: [], emoji_reactions: [],
parent_visible: false, parent_visible: false
local_only: false
} }
} }