Add ability to auto-approve followbacks

Resolves: AkkomaGang/akkoma#148
This commit is contained in:
Oneric 2024-01-17 19:13:29 +00:00
parent 13e62b4e51
commit 376f6b15ca
11 changed files with 80 additions and 12 deletions

View file

@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Added ## Added
- Full compatibility with Erlang OTP26 - Full compatibility with Erlang OTP26
- handling of GET /api/v1/preferences - handling of GET /api/v1/preferences
- ability to auto-approve follow requests from users you are already following
## Changed ## Changed
- OTP builds are now built on erlang OTP26 - OTP builds are now built on erlang OTP26

View file

@ -117,6 +117,7 @@ Has these additional fields under the `akkoma` object:
- `instance`: nullable object with metadata about the users instance - `instance`: nullable object with metadata about the users instance
- `status_ttl_days`: nullable int, default time after which statuses are deleted - `status_ttl_days`: nullable int, default time after which statuses are deleted
- `permit_followback`: boolean, whether follows from followed accounts are auto-approved
### Source ### Source

View file

@ -160,6 +160,7 @@ defmodule Pleroma.User do
field(:last_status_at, :naive_datetime) field(:last_status_at, :naive_datetime)
field(:language, :string) field(:language, :string)
field(:status_ttl_days, :integer, default: nil) field(:status_ttl_days, :integer, default: nil)
field(:permit_followback, :boolean, default: false)
field(:accepts_direct_messages_from, Ecto.Enum, field(:accepts_direct_messages_from, Ecto.Enum,
values: [:everybody, :people_i_follow, :nobody], values: [:everybody, :people_i_follow, :nobody],
@ -544,6 +545,7 @@ def update_changeset(struct, params \\ %{}) do
:actor_type, :actor_type,
:disclose_client, :disclose_client,
:status_ttl_days, :status_ttl_days,
:permit_followback,
:accepts_direct_messages_from :accepts_direct_messages_from
] ]
) )
@ -972,16 +974,21 @@ def needs_update?(%User{local: false} = user) do
def needs_update?(_), do: true def needs_update?(_), do: true
# "Locked" (self-locked) users demand explicit authorization of follow requests
@spec can_direct_follow_local(User.t(), User.t()) :: true | false
def can_direct_follow_local(%User{} = follower, %User{local: true} = followed) do
!followed.is_locked || (followed.permit_followback and is_friend_of(follower, followed))
end
@spec maybe_direct_follow(User.t(), User.t()) :: @spec maybe_direct_follow(User.t(), User.t()) ::
{:ok, User.t(), User.t()} | {:error, String.t()} {:ok, User.t(), User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, is_locked: true} = followed) do
follow(follower, followed, :follow_pending)
end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
follow(follower, followed) if can_direct_follow_local(follower, followed) do
follow(follower, followed)
else
follow(follower, followed, :follow_pending)
end
end end
def maybe_direct_follow(%User{} = follower, %User{} = followed) do def maybe_direct_follow(%User{} = follower, %User{} = followed) do
@ -1331,6 +1338,13 @@ def get_friends_ids(%User{} = user, page \\ nil) do
|> Repo.all() |> Repo.all()
end end
def is_friend_of(%User{} = potential_friend, %User{local: true} = user) do
user
|> get_friends_query()
|> where(id: ^potential_friend.id)
|> Repo.exists?()
end
def increase_note_count(%User{} = user) do def increase_note_count(%User{} = user) do
User User
|> where(id: ^user.id) |> where(id: ^user.id)

View file

@ -109,7 +109,7 @@ def handle(
%User{} = followed <- User.get_cached_by_ap_id(followed_user), %User{} = followed <- User.get_cached_by_ap_id(followed_user),
{_, {:ok, _, _}, _, _} <- {_, {:ok, _, _}, _, _} <-
{:following, User.follow(follower, followed, :follow_pending), follower, followed} do {:following, User.follow(follower, followed, :follow_pending), follower, followed} do
if followed.local && !followed.is_locked do if followed.local && User.can_direct_follow_local(follower, followed) do
{:ok, accept_data, _} = Builder.accept(followed, object) {:ok, accept_data, _} = Builder.accept(followed, object)
{:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true) {:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true)
end end

View file

@ -723,6 +723,12 @@ defp update_credentials_request do
description: description:
"Number of days after which statuses will be deleted. Set to -1 to disable." "Number of days after which statuses will be deleted. Set to -1 to disable."
}, },
permit_followback: %Schema{
allOf: [BooleanLike],
nullable: true,
description:
"Whether follow requests from accounts the user is already following are auto-approved (when locked)."
},
accepts_direct_messages_from: %Schema{ accepts_direct_messages_from: %Schema{
type: :string, type: :string,
enum: [ enum: [
@ -754,6 +760,7 @@ defp update_credentials_request do
discoverable: false, discoverable: false,
actor_type: "Person", actor_type: "Person",
status_ttl_days: 30, status_ttl_days: 30,
permit_followback: true,
accepts_direct_messages_from: "everybody" accepts_direct_messages_from: "everybody"
} }
} }

View file

@ -122,7 +122,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
nodeinfo: %Schema{type: :object, nullable: true} nodeinfo: %Schema{type: :object, nullable: true}
} }
}, },
status_ttl_days: %Schema{type: :integer, nullable: true} status_ttl_days: %Schema{type: :integer, nullable: true},
permit_followback: %Schema{type: :boolean}
} }
}, },
source: %Schema{ source: %Schema{
@ -224,7 +225,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
# XXX: nodeinfo schema # XXX: nodeinfo schema
} }
}, },
"status_ttl_days" => nil "status_ttl_days" => nil,
"permit_followback" => true
}, },
"source" => %{ "source" => %{
"fields" => [], "fields" => [],

View file

@ -222,6 +222,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language])) |> 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(:status_ttl_days, params[:status_ttl_days], status_ttl_days_value)
|> Maps.put_if_present(:accepts_direct_messages_from, params[:accepts_direct_messages_from]) |> Maps.put_if_present(:accepts_direct_messages_from, params[:accepts_direct_messages_from])
|> Maps.put_if_present(:permit_followback, params[:permit_followback])
# What happens here: # What happens here:
# #

View file

@ -292,7 +292,8 @@ defp do_render("show.json", %{user: user} = opts) do
last_status_at: user.last_status_at, last_status_at: user.last_status_at,
akkoma: %{ akkoma: %{
instance: render("instance.json", %{instance: instance}), instance: render("instance.json", %{instance: instance}),
status_ttl_days: user.status_ttl_days status_ttl_days: user.status_ttl_days,
permit_followback: user.permit_followback
}, },
# Pleroma extensions # Pleroma extensions
# Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub # Note: it's insecure to output :email but fully-qualified nickname may serve as safe stub

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddPermitFollowback do
use Ecto.Migration
def change do
alter table(:users) do
add(:permit_followback, :boolean, null: false, default: false)
end
end
end

View file

@ -1061,6 +1061,36 @@ test "directly follows a non-locked local user" do
assert User.following?(follower, followed) assert User.following?(follower, followed)
end end
test "directly follows back a locked, but followback-allowing local user" do
uopen = insert(:user, is_locked: false)
uselective = insert(:user, is_locked: true, permit_followback: true)
assert {:ok, uselective, uopen, %{data: %{"state" => "accept"}}} =
CommonAPI.follow(uselective, uopen)
assert User.get_follow_state(uselective, uopen) == :follow_accept
assert {:ok, uopen, uselective, %{data: %{"state" => "accept"}}} =
CommonAPI.follow(uopen, uselective)
assert User.get_follow_state(uopen, uselective) == :follow_accept
end
test "creates a pending request for locked, non-followback local user" do
uopen = insert(:user, is_locked: false)
ulocked = insert(:user, is_locked: true, permit_followback: false)
assert {:ok, ulocked, uopen, %{data: %{"state" => "accept"}}} =
CommonAPI.follow(ulocked, uopen)
assert User.get_follow_state(ulocked, uopen) == :follow_accept
assert {:ok, uopen, ulocked, %{data: %{"state" => "pending"}}} =
CommonAPI.follow(uopen, ulocked)
assert User.get_follow_state(uopen, ulocked) == :follow_pending
end
end end
describe "unfollow/2" do describe "unfollow/2" do

View file

@ -65,7 +65,8 @@ test "Represent a user account" do
}, },
favicon: nil favicon: nil
}, },
status_ttl_days: 5 status_ttl_days: 5,
permit_followback: false
}, },
avatar: "http://localhost:4001/images/avi.png", avatar: "http://localhost:4001/images/avi.png",
avatar_static: "http://localhost:4001/images/avi.png", avatar_static: "http://localhost:4001/images/avi.png",
@ -248,7 +249,8 @@ test "Represent a Service(bot) account" do
favicon: "http://localhost:4001/favicon.png", favicon: "http://localhost:4001/favicon.png",
nodeinfo: %{version: "2.0"} nodeinfo: %{version: "2.0"}
}, },
status_ttl_days: nil status_ttl_days: nil,
permit_followback: false
}, },
pleroma: %{ pleroma: %{
ap_id: user.ap_id, ap_id: user.ap_id,